AWS Logging & Detection Hardening

Overview

This page covers Amazon Web Services logging and detection across the surfaces that decide whether an attacker who lands in the environment can move undetected. Scope is the AWS commercial regions; AWS GovCloud (US) and the China regions inherit the same controls but expose different region endpoints, different CloudTrail home regions, and different partitions — re-verify the regional table before applying any of the IaC below to a non-commercial partition. CIS sub-IDs and NIST / ISO mappings throughout this page reference the AWS commercial benchmark unless explicitly annotated as a post-v3.0.0 feature or a best-practice recommendation that the current benchmark has not yet codified.

The AWS detective stack is the product of six layered services that each answer a different question. AWS CloudTrail answers who called which API at what time — the audit log of every management-plane and (when configured) data-plane call across the account. AWS Config answers what does the resource look like right now and how has it changed — a resource-state inventory plus configuration-history timeline. Amazon GuardDuty answers does this look like an attack — managed threat detection that correlates CloudTrail, VPC Flow Logs, DNS query logs, EKS audit logs, and Malware Protection findings against AWS's threat-intelligence feeds. AWS Security Hub answers what is broken across our entire estate against an external standard — a finding aggregator that ingests GuardDuty, Inspector, Macie, IAM Access Analyzer, and Config rules and benchmarks them against CIS, AWS Foundational Security Best Practices (FSBP), and PCI DSS. VPC Flow Logs answer what packets moved between which ENIs — a network-layer record that fills the gap CloudTrail leaves between API calls. Amazon CloudWatch answers did the metric we care about just cross a threshold — the metric-and-alarm layer that closes the loop from log to page. These six together provide the full record; cross-link the cross-cutting principles at General Logging — log integrity (immutable audit), centralization, and SIEM & detection engineering.

Order matters. Controls 01–02 build the audit log of last resort: one org-wide CloudTrail with all six toggles (multi-region, organization-trail, log-file-validation, KMS-encrypted, S3 Object Lock Compliance mode, management plus S3 plus Lambda data events) so the canonical "who did what" record survives both attacker tampering and accidental deletion. Control 03 enables Config across every region of every account so resource-state history is captured continuously. Control 04 turns on GuardDuty as the managed threat-detection engine. Control 05 layers Security Hub on top to aggregate findings and benchmark them against external standards. Control 06 covers VPC Flow Logs for the network-layer record that CloudTrail does not produce. Control 07 puts CloudWatch alarms on the canonical "things-went-wrong" signals so detection actually pages a human. Control 08 ships CloudTrail Lake as the forensic SQL store — paired with aws-ir-04 on the IR page, which walks the forensic query workflow that consumes this store.

Pairing note: the GuardDuty → Security Hub → EventBridge pipeline is the operational backbone of detection-and-response. GuardDuty findings flow into Security Hub via the built-in integration; Security Hub findings (and CRITICAL GuardDuty findings directly) drive EventBridge rules that trigger automated containment as aws-ir-02 on the IR page. Treat this page (which establishes the signal sources) and the IR page (which acts on them) as a single design pair.

aws-log-01-cloudtrail-org-trail ! CRITICAL DETECTIVE

Run exactly one organization-wide AWS CloudTrail with all six hardening toggles simultaneously enabled: (1) multi-region so every region's API activity is captured even in regions the organisation does not consciously use; (2) organization-trail so every member account in the AWS Organization is recorded under a single trail definition the member cannot disable; (3) log-file-validation so each delivered log file carries an HMAC-signed digest CloudTrail can verify offline; (4) KMS-encrypted with a customer-managed CMK so log access requires both S3 read and KMS Decrypt; (5) S3 Object Lock in Compliance mode on the destination bucket with at least a one-year retention so an attacker (or a panicked engineer) cannot delete or overwrite log objects even with root credentials; (6) event selectors covering management events plus S3 data events plus Lambda data events so the trail records the data-plane calls that management events alone miss (AWS CloudTrail User Guide — creating a trail (accessed 2026-05)). Missing any of these six is the canonical CloudTrail misconfiguration; the IaC below shows them as a single coherent unit. This is the audit log of last resort — without it, incident response is reconstructing intent from secondary signals.

Remediation — AWS CLI

# Pre-step: create the KMS CMK for trail encryption (separate region per partition).
aws kms create-key \
  --description "CloudTrail org trail encryption CMK" \
  --key-usage ENCRYPT_DECRYPT

# Create the org trail with all six hardening toggles in one call.
aws cloudtrail create-trail \
  --name org-audit-trail \
  --s3-bucket-name org-cloudtrail-logs-111122223333 \
  --is-multi-region-trail \
  --is-organization-trail \
  --enable-log-file-validation \
  --kms-key-id arn:aws:kms:eu-west-1:111122223333:key/<cmk-id>

# Event selectors: management + S3 data + Lambda data events.
aws cloudtrail put-event-selectors \
  --trail-name org-audit-trail \
  --advanced-event-selectors '[
    {"Name":"Management events","FieldSelectors":[
      {"Field":"eventCategory","Equals":["Management"]}]},
    {"Name":"All S3 data events","FieldSelectors":[
      {"Field":"eventCategory","Equals":["Data"]},
      {"Field":"resources.type","Equals":["AWS::S3::Object"]}]},
    {"Name":"All Lambda data events","FieldSelectors":[
      {"Field":"eventCategory","Equals":["Data"]},
      {"Field":"resources.type","Equals":["AWS::Lambda::Function"]}]}
  ]'

# Start logging.
aws cloudtrail start-logging --name org-audit-trail

Remediation — Terraform

# Terraform AWS provider ~> 5.0
# Source: AWS docs (accessed 2026-05)
# Destination bucket: Object Lock Compliance mode, 365-day retention.
resource "aws_s3_bucket" "trail" {
  bucket              = "org-cloudtrail-logs-${var.payer_account_id}"
  object_lock_enabled = true
}

resource "aws_s3_bucket_object_lock_configuration" "trail" {
  bucket = aws_s3_bucket.trail.id
  rule {
    default_retention {
      mode = "COMPLIANCE"
      days = 365
    }
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "trail" {
  bucket = aws_s3_bucket.trail.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.trail.arn
    }
  }
}

resource "aws_kms_key" "trail" {
  description             = "CloudTrail org trail CMK"
  enable_key_rotation     = true
  deletion_window_in_days = 30
}

resource "aws_cloudtrail" "org" {
  name                          = "org-audit-trail"
  s3_bucket_name                = aws_s3_bucket.trail.id
  is_multi_region_trail         = true
  is_organization_trail         = true
  enable_log_file_validation    = true
  kms_key_id                    = aws_kms_key.trail.arn
  include_global_service_events = true

  advanced_event_selector {
    name = "Management events"
    field_selector {
      field  = "eventCategory"
      equals = ["Management"]
    }
  }
  advanced_event_selector {
    name = "All S3 data events"
    field_selector {
      field  = "eventCategory"
      equals = ["Data"]
    }
    field_selector {
      field  = "resources.type"
      equals = ["AWS::S3::Object"]
    }
  }
  advanced_event_selector {
    name = "All Lambda data events"
    field_selector {
      field  = "eventCategory"
      equals = ["Data"]
    }
    field_selector {
      field  = "resources.type"
      equals = ["AWS::Lambda::Function"]
    }
  }
}

Remediation — CloudFormation

AWSTemplateFormatVersion: '2010-09-09'
Description: Organization-wide CloudTrail multi-region trail with log-file validation and KMS-encrypted destination.
Parameters:
  TrailBucketName:
    Type: String
  TrailKmsKeyArn:
    Type: String
Resources:
  OrgTrail:
    Type: AWS::CloudTrail::Trail
    Properties:
      TrailName: org-management-events
      S3BucketName: !Ref TrailBucketName
      IsLogging: true
      IsMultiRegionTrail: true
      IsOrganizationTrail: true
      EnableLogFileValidation: true
      IncludeGlobalServiceEvents: true
      KMSKeyId: !Ref TrailKmsKeyArn

Remediation — AWS CDK (TypeScript)

import * as cdk from 'aws-cdk-lib';
import { aws_cloudtrail as ct, aws_s3 as s3, aws_kms as kms } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export interface OrgTrailProps extends cdk.StackProps {
  trailBucketName: string;
  trailKmsKeyArn: string;
}

export class OrgTrailStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: OrgTrailProps) {
    super(scope, id, props);

    const bucket = s3.Bucket.fromBucketName(this, 'TrailBucket', props.trailBucketName);
    const key = kms.Key.fromKeyArn(this, 'TrailKey', props.trailKmsKeyArn);

    new ct.Trail(this, 'OrgTrail', {
      trailName: 'org-management-events',
      bucket,
      encryptionKey: key,
      isMultiRegionTrail: true,
      isOrganizationTrail: true,
      enableFileValidation: true,
      includeGlobalServiceEvents: true,
    });
  }
}

Compliance mapping

CIS AWS Foundations v3.0.0 CIS Microsoft Azure Foundations v3.0.0 CIS GCP Foundation v4.0.0 CIS OCI Foundation v2.0.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015
3.1; 3.2; 3.4; 3.55.1; 5.22.1; 2.23.1; 3.2 AU-2; AU-3; AU-6; AU-9A.8.15; A.5.28CLD.12.4.5

Log signals

  • CloudTrail cloudtrail:StopLogging on the organisation trail's ARN — the most direct evasion path; once stopped, subsequent API activity does not land in the org sink for the duration the trail remains stopped.
  • CloudTrail cloudtrail:UpdateTrail where requestParameters.isMultiRegionTrail flips to false, or where requestParameters.isOrganizationTrail changes from true to false — silently narrows the trail's scope without stopping it.
  • CloudTrail cloudtrail:DeleteTrail targeting the org-trail name; correlate with concurrent iam:CreateRole or sts:AssumeRole events in the same management-account session as an attacker preparing follow-on activity.

Query

fields @timestamp, eventName, requestParameters.name, requestParameters.isMultiRegionTrail, requestParameters.isOrganizationTrail, userIdentity.arn, sourceIPAddress, errorCode
          | filter eventSource = "cloudtrail.amazonaws.com" and eventName in ["StopLogging","UpdateTrail","DeleteTrail"]
          | filter requestParameters.name = "org-management-trail" or requestParameters.name like /-org-trail$/
          | sort @timestamp desc
          | limit 50

Pin the CloudWatch Logs Insights filter to the canonical trail name(s) maintained in the management-account IaC; trail names are durable identifiers and a name mismatch is itself a signal that an unexpected trail-management call landed.

Alert threshold

  • Any StopLogging on the org trail — page immediately and treat as a confirmed evasion attempt; the steady-state rate of legitimate stops is zero.
  • UpdateTrail flipping isMultiRegionTrail or isOrganizationTrail to false — page within five minutes; the scope contraction is the equivalent of stopping for the affected regions/accounts.
  • DeleteTrail — page immediately and trigger automatic re-creation from the management-account Terraform state, then audit every CloudTrail event in the trail's S3 bucket for the prior 24 hours before the delete to bound the forensic window.

Initial response

  1. Re-start the trail with aws cloudtrail start-logging --name {trail-arn} from the management account; if deleted, run the Terraform apply that re-creates the org trail rather than recreating manually so configuration drift closes at the source.
  2. Pull the CloudTrail Lake (aws-log-08-cloudtrail-lake) event-data-store covering the same window and reconcile gap-of-coverage against the now-restored trail — Lake retention is independent of the live trail and is the canonical forensic source during evasion windows.
  3. Open an incident per general/ir.html and rotate any IAM credentials whose principal performed the trail mutation; the act of touching the trail strongly implies the principal expects to do something next they do not want logged.

References

Equivalent on: Azure · GCP · OCI

aws-log-02-cloudtrail-s3-data-events ! HIGH DETECTIVE

Enable CloudTrail data events on every S3 bucket that holds sensitive content (PII, regulated data, audit logs themselves, secrets backups) and on every customer-facing or privilege-elevated Lambda function. Management events alone record bucket-level calls — CreateBucket, PutBucketPolicy, DeleteBucket — but they do not record GetObject or PutObject against bucket contents. Without data events, an attacker who lifts an IAM role with s3:GetObject on a sensitive bucket can exfiltrate every object without leaving any CloudTrail entry that names the objects accessed (AWS CloudTrail User Guide — logging data events (accessed 2026-05)). The same gap exists for Lambda: management events log CreateFunction and UpdateFunctionCode, but not the per-invocation record of which principal invoked the function with which payload. Note: data events are billed per event delivered, so blanket enablement across petabyte-scale data lakes can be cost-prohibitive — scope event selectors to the buckets where the audit-trail value justifies the cost (PII buckets, audit-log buckets, secrets-manager backup buckets, anywhere a single unauthorized read is a security incident).

Remediation — AWS CLI

# Add S3 data events on the PII bucket and Lambda data events on customer-facing fns.
aws cloudtrail put-event-selectors \
  --trail-name org-audit-trail \
  --advanced-event-selectors '[
    {"Name":"Management","FieldSelectors":[
      {"Field":"eventCategory","Equals":["Management"]}]},
    {"Name":"S3 sensitive bucket data events","FieldSelectors":[
      {"Field":"eventCategory","Equals":["Data"]},
      {"Field":"resources.type","Equals":["AWS::S3::Object"]},
      {"Field":"resources.ARN","StartsWith":[
        "arn:aws:s3:::pii-prod-eu-west-1/",
        "arn:aws:s3:::audit-archive-eu-west-1/"]}]},
    {"Name":"Lambda customer-facing data events","FieldSelectors":[
      {"Field":"eventCategory","Equals":["Data"]},
      {"Field":"resources.type","Equals":["AWS::Lambda::Function"]},
      {"Field":"resources.ARN","StartsWith":[
        "arn:aws:lambda:eu-west-1:111122223333:function:public-api-"]}]}
  ]'

Remediation — Terraform

# Terraform AWS provider ~> 5.0
# Source: AWS docs (accessed 2026-05)
# Scoped data events as a second trail (keeps the org trail's blanket selector
# unaffected while letting per-bucket selectors evolve independently).
resource "aws_cloudtrail" "sensitive_data_events" {
  name                          = "sensitive-data-events"
  s3_bucket_name                = aws_s3_bucket.trail.id
  is_multi_region_trail         = true
  enable_log_file_validation    = true
  kms_key_id                    = aws_kms_key.trail.arn

  event_selector {
    read_write_type           = "All"
    include_management_events = false

    data_resource {
      type = "AWS::S3::Object"
      values = [
        "arn:aws:s3:::pii-prod-eu-west-1/",
        "arn:aws:s3:::audit-archive-eu-west-1/",
      ]
    }
  }

  event_selector {
    read_write_type           = "All"
    include_management_events = false

    data_resource {
      type = "AWS::Lambda::Function"
      values = [
        "arn:aws:lambda:eu-west-1:111122223333:function:public-api-checkout",
        "arn:aws:lambda:eu-west-1:111122223333:function:public-api-auth",
      ]
    }
  }
}

Remediation — CloudFormation

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudTrail trail capturing S3 object-level data events on sensitive buckets.
Parameters:
  TrailBucketName:
    Type: String
  SensitiveBucketArn:
    Type: String
Resources:
  S3DataEventsTrail:
    Type: AWS::CloudTrail::Trail
    Properties:
      TrailName: s3-data-events
      S3BucketName: !Ref TrailBucketName
      IsLogging: true
      IsMultiRegionTrail: true
      AdvancedEventSelectors:
        - Name: S3 object-level reads/writes
          FieldSelectors:
            - Field: eventCategory
              Equals: [Data]
            - Field: resources.type
              Equals: [AWS::S3::Object]
            - Field: resources.ARN
              StartsWith: [!Ref SensitiveBucketArn]

Compliance mapping

CIS AWS Foundations v3.0.0 CIS Microsoft Azure Foundations v3.0.0 CIS GCP Foundation v4.0.0 CIS OCI Foundation v2.0.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015
3.x (verify)5.x (verify)2.x (verify)3.x (verify) AU-2; AU-12A.8.15CLD.12.4.5

Log signals

  • CloudTrail PutEventSelectors events removing S3 data-event selectors — drops object-level visibility on previously-monitored buckets without touching the management-event trail itself; the most common silent regression.
  • PutEventSelectors where the dataResources.values array no longer includes the canonical arn:aws:s3:::*/ prefix or a documented per-bucket list — narrows coverage to a subset and lets the operator quietly exclude buckets they expect to access.
  • Absence-of-signal: the S3 data-event ingestion volume (per-bucket per-day object-level events) drops to zero on a bucket that historically receives traffic, with no corresponding drop in S3 server-access-log records for that bucket.

Query

fields @timestamp, eventName, requestParameters.trailName, requestParameters.eventSelectors, requestParameters.advancedEventSelectors, userIdentity.arn
          | filter eventSource = "cloudtrail.amazonaws.com" and eventName = "PutEventSelectors"
          | filter requestParameters.trailName like /-org-trail$/ or requestParameters.trailName like /-data-events-trail$/
          | sort @timestamp desc
          | limit 50

Pair the CloudWatch Logs Insights query with a daily diff job that captures the current GetEventSelectors output and compares against the previous day's snapshot; the diff is the most reliable signal because PutEventSelectors overwrites the entire selector list and the CloudTrail event payload reflects the post-state only.

Alert threshold

  • Any PutEventSelectors on the data-events trail that reduces the resource-ARN coverage — page immediately; the reduction is the policy change and warrants reverting to the prior selectors via IaC.
  • A new selector with readWriteType=WriteOnly introduced on a bucket previously monitored as All — high-priority ticket; the read-side is the canonical exfiltration signal and dropping it is a frequent evasion pattern.
  • Per-bucket S3 data-event ingestion rate falling below 5% of the 7-day baseline while S3 server-access logs remain steady — informational; promote to incident if the divergence persists for two consecutive 1-hour windows.

Initial response

  1. Restore selectors from the IaC repository with aws cloudtrail put-event-selectors --trail-name {arn} --event-selectors file://canonical-selectors.json; confirm via get-event-selectors read-back before closing the ticket.
  2. Use CloudTrail Lake to reconstruct the S3 data-event view for the affected bucket over the coverage-gap window — Lake captures data events independently of the live trail's selectors if the Lake event-data-store was configured for them.
  3. Open an incident via general/ir.html if the gap aligns with any S3 server-access-log entries showing GetObject from non-corporate CIDRs; treat each such object as a candidate exfiltration target and notify data-owners per the bucket's classification tag.

References

Equivalent on: Azure · GCP · OCI

aws-log-03-config-org-enabled ! HIGH DETECTIVE

Enable AWS Config in every region of every account in the organization, with both a Configuration Recorder (capturing every supported resource type) and a Delivery Channel (shipping snapshots and configuration-history items to a central S3 bucket), and apply a Conformance Pack of detective rules that benchmark resource state against the organisation's policy (AWS Config Developer Guide — conformance packs (accessed 2026-05)). Config answers a question CloudTrail cannot: what does this resource look like right now, and how has it changed. CloudTrail records the API calls; Config records the resulting resource state and runs continuous evaluation against rules. The two are complementary — CloudTrail is the timeline of intent, Config is the timeline of result. The conformance-pack approach is the AWS-recommended pattern for declaring "these N rules must evaluate non-compliant resources to NON_COMPLIANT and create a finding"; it ships as a single YAML document so the policy set is version-controlled in the same repo as the rest of the landing-zone. Without Config the organisation has no resource-state inventory, no continuous-compliance signal, and no input for the resource-change correlations that GuardDuty (aws-log-04) and Security Hub (aws-log-05) consume.

Remediation — AWS CLI

# Enable the recorder (capture every supported resource type and global resources).
aws configservice put-configuration-recorder --configuration-recorder '{
  "name":"default",
  "roleARN":"arn:aws:iam::111122223333:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig",
  "recordingGroup":{
    "allSupported":true,
    "includeGlobalResourceTypes":true}}'

# Delivery channel: ship to the central S3 bucket.
aws configservice put-delivery-channel --delivery-channel '{
  "name":"default",
  "s3BucketName":"org-config-history-111122223333",
  "configSnapshotDeliveryProperties":{"deliveryFrequency":"One_Hour"}}'

# Start recording.
aws configservice start-configuration-recorder --configuration-recorder-name default

# Deploy a conformance pack across the organization.
aws configservice put-organization-conformance-pack \
  --organization-conformance-pack-name org-security-baseline \
  --template-s3-uri s3://org-config-packs/security-baseline.yaml

Remediation — Terraform

# Terraform AWS provider ~> 5.0
# Source: AWS docs (accessed 2026-05)
resource "aws_config_configuration_recorder" "this" {
  name     = "default"
  role_arn = aws_iam_role.config.arn

  recording_group {
    all_supported                 = true
    include_global_resource_types = true
  }
}

resource "aws_config_delivery_channel" "this" {
  name           = "default"
  s3_bucket_name = aws_s3_bucket.config.id
  snapshot_delivery_properties {
    delivery_frequency = "One_Hour"
  }
  depends_on = [aws_config_configuration_recorder.this]
}

resource "aws_config_configuration_recorder_status" "this" {
  name       = aws_config_configuration_recorder.this.name
  is_enabled = true
  depends_on = [aws_config_delivery_channel.this]
}

resource "aws_config_organization_conformance_pack" "baseline" {
  name              = "org-security-baseline"
  template_s3_uri   = "s3://org-config-packs/security-baseline.yaml"
  delivery_s3_bucket = aws_s3_bucket.config.id
}

Remediation — CloudFormation

AWSTemplateFormatVersion: '2010-09-09'
Description: AWS Config recorder and delivery channel for all-resource recording in the account.
Parameters:
  ConfigRoleArn:
    Type: String
  ConfigBucketName:
    Type: String
Resources:
  ConfigRecorder:
    Type: AWS::Config::ConfigurationRecorder
    Properties:
      Name: org-config-recorder
      RoleARN: !Ref ConfigRoleArn
      RecordingGroup:
        AllSupported: true
        IncludeGlobalResourceTypes: true
  ConfigDeliveryChannel:
    Type: AWS::Config::DeliveryChannel
    Properties:
      Name: org-config-channel
      S3BucketName: !Ref ConfigBucketName

Compliance mapping

CIS AWS Foundations v3.0.0 CIS Microsoft Azure Foundations v3.0.0 CIS GCP Foundation v4.0.0 CIS OCI Foundation v2.0.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015
3.x (verify)n/an/an/a CM-8; CM-3A.8.9CLD.12.4.5

Log signals

  • CloudTrail config:StopConfigurationRecorder events targeting the org-level recorder — halts per-resource compliance evaluation; downstream Config-rule alarms then go silent rather than firing, which is the worst evasion outcome because absence of alerts is interpreted as cleanliness.
  • CloudTrail config:DeleteConfigurationRecorder or config:DeleteDeliveryChannel — destroys the recorder/sink rather than pausing it; equally bad from a coverage standpoint and additionally erases the delivery configuration that the IaC re-apply needs as input.
  • CloudTrail config:PutConfigurationRecorder where recordingGroup.allSupported flips from true to false, or where recordingGroup.resourceTypes drops resource types previously enumerated — narrows recorder scope silently.

Query

fields @timestamp, eventName, requestParameters.configurationRecorderName, requestParameters.recordingGroup.allSupported, requestParameters.recordingGroup.resourceTypes, userIdentity.arn
          | filter eventSource = "config.amazonaws.com" and eventName in ["StopConfigurationRecorder","DeleteConfigurationRecorder","DeleteDeliveryChannel","PutConfigurationRecorder"]
          | sort @timestamp desc
          | limit 100

Run the CloudWatch Logs Insights query against the management-account org-trail log group; Config service events fan in there because the recorder is configured at the delegated-administrator account scope.

Alert threshold

  • Any StopConfigurationRecorder or DeleteConfigurationRecorder in production — page immediately; the recorder is a tenancy-wide control and disabling it blinds every Config rule downstream.
  • PutConfigurationRecorder flipping allSupported to false — high-priority ticket; the narrowed scope might be a deliberate cost optimisation but should never land outside a tracked change.
  • DeleteDeliveryChannel events — page; without a delivery channel the recorded snapshots cannot land in S3 even if the recorder is still running, producing a delayed-failure profile that masquerades as healthy.

Initial response

  1. Re-start the recorder with aws configservice start-configuration-recorder --configuration-recorder-name {name} or re-create from IaC if deleted; confirm describe-configuration-recorder-status reports lastStatus=SUCCESS before clearing the alarm.
  2. Trigger a fleet-wide re-evaluation with aws configservice start-config-rules-evaluation on all org-managed rules so the gap-in-coverage is closed and any newly-non-compliant resources surface within minutes rather than at the next scheduled evaluation.
  3. Open an incident per general/ir.html and inventory the resources created during the recorder-off window via CloudTrail RunInstances, CreateBucket, CreateDBInstance, etc. — these resources were created without Config-rule evaluation and need a manual posture review before they enter steady-state.

References

Equivalent on: Azure · GCP · OCI

aws-log-04-guardduty-org ! CRITICAL DETECTIVE

Enable Amazon GuardDuty across the entire AWS Organization via a delegated administrator account, with every data source turned on as a single umbrella: CloudTrail management-event analysis (always-on), VPC Flow Logs and DNS query log analysis, S3 protection (S3 data-event analysis), EKS protection (EKS audit log analysis), Malware Protection (agentless EBS volume scanning), RDS Protection (login-event analysis on Aurora MySQL and Aurora PostgreSQL), and Lambda Protection (network activity analysis) (Amazon GuardDuty User Guide — what is GuardDuty (accessed 2026-05)). GuardDuty is managed threat detection — AWS runs and tunes the detection models, AWS curates the threat-intelligence feeds, and the organisation only pays for the data analysed. There is no on-prem analogue to switch off, no rule library to maintain, and no reason to operate the data sources as separately-enabled controls — they share a single detector resource, a single delegated-admin model, and a single org-configuration. Treat the data sources as facets of one umbrella control; do not split into five sub-controls (Open Question §8 resolution). GuardDuty surfaces findings under broad categories — Recon:*, UnauthorizedAccess:*, Backdoor:*, Trojan:*, Stealth:*, CryptoCurrency:*, Impact:*, PenTest:*, Policy:* — and the underlying full finding-type strings change as AWS adds detectors, so reference the category prefixes rather than enumerating the full finding-type list (Pitfall 9). GuardDuty findings flow into Security Hub by default and into EventBridge as native events.

Remediation — AWS CLI

# In the Organizations management account: delegate GuardDuty admin.
aws guardduty enable-organization-admin-account \
  --admin-account-id 222233334444

# In the delegated-admin account: enable the detector with all features.
DETECTOR_ID=$(aws guardduty create-detector \
  --enable \
  --finding-publishing-frequency FIFTEEN_MINUTES \
  --features \
    Name=S3_DATA_EVENTS,Status=ENABLED \
    Name=EKS_AUDIT_LOGS,Status=ENABLED \
    Name=EBS_MALWARE_PROTECTION,Status=ENABLED \
    Name=RDS_LOGIN_EVENTS,Status=ENABLED \
    Name=LAMBDA_NETWORK_LOGS,Status=ENABLED \
  --query DetectorId --output text)

# Auto-enrol every existing and future member account.
aws guardduty update-organization-configuration \
  --detector-id "$DETECTOR_ID" \
  --auto-enable-organization-members ALL \
  --features \
    Name=S3_DATA_EVENTS,AutoEnable=NEW \
    Name=EKS_AUDIT_LOGS,AutoEnable=NEW \
    Name=EBS_MALWARE_PROTECTION,AutoEnable=NEW \
    Name=RDS_LOGIN_EVENTS,AutoEnable=NEW \
    Name=LAMBDA_NETWORK_LOGS,AutoEnable=NEW

Remediation — Terraform

# Terraform AWS provider ~> 5.0
# Source: AWS docs (accessed 2026-05)
# In the Organizations management account.
resource "aws_guardduty_organization_admin_account" "this" {
  admin_account_id = var.security_account_id
}

# In the delegated-admin (security) account.
resource "aws_guardduty_detector" "this" {
  enable                       = true
  finding_publishing_frequency = "FIFTEEN_MINUTES"
}

resource "aws_guardduty_detector_feature" "s3" {
  detector_id = aws_guardduty_detector.this.id
  name        = "S3_DATA_EVENTS"
  status      = "ENABLED"
}

resource "aws_guardduty_detector_feature" "eks" {
  detector_id = aws_guardduty_detector.this.id
  name        = "EKS_AUDIT_LOGS"
  status      = "ENABLED"
}

resource "aws_guardduty_detector_feature" "ebs_malware" {
  detector_id = aws_guardduty_detector.this.id
  name        = "EBS_MALWARE_PROTECTION"
  status      = "ENABLED"
}

resource "aws_guardduty_detector_feature" "rds" {
  detector_id = aws_guardduty_detector.this.id
  name        = "RDS_LOGIN_EVENTS"
  status      = "ENABLED"
}

resource "aws_guardduty_detector_feature" "lambda" {
  detector_id = aws_guardduty_detector.this.id
  name        = "LAMBDA_NETWORK_LOGS"
  status      = "ENABLED"
}

resource "aws_guardduty_organization_configuration" "this" {
  detector_id                      = aws_guardduty_detector.this.id
  auto_enable_organization_members = "ALL"
}

Remediation — CloudFormation

AWSTemplateFormatVersion: '2010-09-09'
Description: GuardDuty detector with all current protection plans enabled (S3, EKS audit + runtime, malware, RDS, Lambda).
Resources:
  GuardDutyDetector:
    Type: AWS::GuardDuty::Detector
    Properties:
      Enable: true
      FindingPublishingFrequency: FIFTEEN_MINUTES
      Features:
        - Name: S3_DATA_EVENTS
          Status: ENABLED
        - Name: EKS_AUDIT_LOGS
          Status: ENABLED
        - Name: EBS_MALWARE_PROTECTION
          Status: ENABLED
        - Name: RDS_LOGIN_EVENTS
          Status: ENABLED
        - Name: LAMBDA_NETWORK_LOGS
          Status: ENABLED
        - Name: EKS_RUNTIME_MONITORING
          Status: ENABLED
          AdditionalConfiguration:
            - Name: EKS_ADDON_MANAGEMENT
              Status: ENABLED

Remediation — AWS CDK (TypeScript)

import * as cdk from 'aws-cdk-lib';
import { aws_guardduty as gd } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class GuardDutyDetectorStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new gd.CfnDetector(this, 'Detector', {
      enable: true,
      findingPublishingFrequency: 'FIFTEEN_MINUTES',
      features: [
        { name: 'S3_DATA_EVENTS', status: 'ENABLED' },
        { name: 'EKS_AUDIT_LOGS', status: 'ENABLED' },
        { name: 'EBS_MALWARE_PROTECTION', status: 'ENABLED' },
        { name: 'RDS_LOGIN_EVENTS', status: 'ENABLED' },
        { name: 'LAMBDA_NETWORK_LOGS', status: 'ENABLED' },
        {
          name: 'EKS_RUNTIME_MONITORING',
          status: 'ENABLED',
          additionalConfiguration: [
            { name: 'EKS_ADDON_MANAGEMENT', status: 'ENABLED' },
          ],
        },
      ],
    });
  }
}

Compliance mapping

CIS AWS Foundations v3.0.0 CIS Microsoft Azure Foundations v3.0.0 CIS GCP Foundation v4.0.0 CIS OCI Foundation v2.0.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015
(best-practices; CIS-aligned)n/an/an/a SI-4; SI-4(2); SI-4(4)A.8.16CLD.12.4.5

Log signals

  • CloudTrail guardduty:DeleteDetector on the delegated-administrator detector — destroys the org-wide finding aggregator and breaks fan-out to all member accounts; the most catastrophic single call against this control.
  • CloudTrail guardduty:UpdateDetector where requestParameters.enable is false, or where the dataSources map disables S3 logs, Kubernetes audit logs, malware protection, or RDS login events — silently narrows feature coverage without disabling the detector itself.
  • CloudTrail guardduty:DisassociateMembers or guardduty:DeleteMembers on member accounts — drops accounts out of the org aggregator one at a time, harder to spot than a single global disable.

Query

fields @timestamp, eventName, requestParameters.detectorId, requestParameters.enable, requestParameters.dataSources, requestParameters.accountIds, userIdentity.arn
          | filter eventSource = "guardduty.amazonaws.com" and eventName in ["DeleteDetector","UpdateDetector","DisassociateMembers","DeleteMembers"]
          | sort @timestamp desc
          | limit 100

Filter the CloudWatch Logs Insights query at the GuardDuty delegated-administrator account's CloudTrail log group; cross-account aggregations route through that account and the delegated-admin context is the canonical source of truth for org-level GuardDuty state.

Alert threshold

  • Any DeleteDetector on the delegated-administrator detector — page immediately; the org-wide finding flow stops at the moment of the delete and the rebuild requires a multi-step re-association flow.
  • UpdateDetector with enable=false or any data-source disable — high-priority ticket within 10 minutes; particularly malware-protection and Kubernetes audit logs disables, which are the highest-signal sources for the corresponding controls.
  • DisassociateMembers or DeleteMembers affecting more than one account in a 24-hour window — informational on single occurrences (account-closure flow), page on the second.

Initial response

  1. Restore the detector via the GuardDuty IaC stack apply (Terraform or CloudFormation StackSet); manual recreate via aws guardduty create-detector is acceptable only when the IaC pipeline is also out of action and the gap must be closed immediately.
  2. For disassociated members, re-invite and re-associate with aws guardduty invite-members followed by member-account accept-invitation automation; treat each gap window as a finding-blind period and back-fill via VPC Flow Logs and CloudTrail anomaly search.
  3. Open an incident via general/ir.html and pivot the EventBridge fan-out (aws-ir-02-eventbridge-auto-containment) to confirm the auto-containment pipeline still receives findings post-restoration; un-tested fan-out is a frequent latent failure after detector recreates.

References

Equivalent on: Azure · GCP · OCI

aws-log-05-security-hub-org ! HIGH DETECTIVE

Enable AWS Security Hub across the entire Organization via a delegated administrator account, with at minimum the CIS AWS Foundations Benchmark v3.0.0, the AWS Foundational Security Best Practices (FSBP), and the PCI DSS standards subscribed (AWS Security Hub User Guide — what is Security Hub (accessed 2026-05)). Security Hub is the finding-aggregation layer that ingests outputs from GuardDuty, Amazon Inspector, Amazon Macie, IAM Access Analyzer, Firewall Manager, AWS Config rules, and Health, and normalises them into the AWS Security Finding Format (ASFF). The standards subscription is the operationally interesting half: once subscribed, Security Hub continuously evaluates every account in scope against the standard's controls and produces a compliance score plus a list of failing resources. CIS AWS Foundations v3.0.0 covers the IAM, logging, and monitoring fundamentals; FSBP covers AWS-recommended best practices outside the CIS scope (KMS rotation, RDS public-access, ELB logging); PCI DSS covers cardholder-data-environment controls when applicable. Security Hub findings drive the EventBridge automated-containment pipeline of aws-ir-02; treat Security Hub as the single pane of glass for the detective stack and the trigger source for response automation.

Remediation — AWS CLI

# In the Organizations management account: delegate Security Hub admin.
aws securityhub enable-organization-admin-account \
  --admin-account-id 222233334444

# In the delegated-admin account: enable Security Hub and subscribe standards.
aws securityhub enable-security-hub --enable-default-standards

# Subscribe to CIS AWS Foundations v3.0.0.
aws securityhub batch-enable-standards \
  --standards-subscription-requests StandardsArn=arn:aws:securityhub:eu-west-1::standards/cis-aws-foundations-benchmark/v/3.0.0

# Subscribe to PCI DSS v3.2.1.
aws securityhub batch-enable-standards \
  --standards-subscription-requests StandardsArn=arn:aws:securityhub:eu-west-1::standards/pci-dss/v/3.2.1

# Auto-enrol every member account.
aws securityhub update-organization-configuration --auto-enable

Remediation — Terraform

# Terraform AWS provider ~> 5.0
# Source: AWS docs (accessed 2026-05)
resource "aws_securityhub_organization_admin_account" "this" {
  admin_account_id = var.security_account_id
}

resource "aws_securityhub_account" "this" {
  enable_default_standards = true
}

resource "aws_securityhub_standards_subscription" "cis_v3" {
  standards_arn = "arn:aws:securityhub:${var.region}::standards/cis-aws-foundations-benchmark/v/3.0.0"
  depends_on    = [aws_securityhub_account.this]
}

resource "aws_securityhub_standards_subscription" "fsbp" {
  standards_arn = "arn:aws:securityhub:${var.region}::standards/aws-foundational-security-best-practices/v/1.0.0"
  depends_on    = [aws_securityhub_account.this]
}

resource "aws_securityhub_standards_subscription" "pci_dss" {
  standards_arn = "arn:aws:securityhub:${var.region}::standards/pci-dss/v/3.2.1"
  depends_on    = [aws_securityhub_account.this]
}

resource "aws_securityhub_organization_configuration" "this" {
  auto_enable = true
}

Remediation — CloudFormation

AWSTemplateFormatVersion: '2010-09-09'
Description: Security Hub enabled with the AWS Foundational Security Best Practices standard subscribed.
Resources:
  SecurityHub:
    Type: AWS::SecurityHub::Hub
    Properties:
      AutoEnableControls: true
      ControlFindingGenerator: SECURITY_CONTROL
      EnableDefaultStandards: true

Compliance mapping

CIS AWS Foundations v3.0.0 CIS Microsoft Azure Foundations v3.0.0 CIS GCP Foundation v4.0.0 CIS OCI Foundation v2.0.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015
(best-practices)n/an/an/a CA-7; SI-4A.8.16; A.5.36CLD.12.4.5

Log signals

  • CloudTrail securityhub:BatchDisableStandards calls naming the CIS AWS Foundations Benchmark or AWS Foundational Security Best Practices standard ARNs — drops the rule set that produces the org's compliance findings without removing the Security Hub admin itself.
  • CloudTrail securityhub:UpdateOrganizationConfiguration with autoEnable=false — stops auto-enrolment of new member accounts so future accounts land outside the org aggregator silently.
  • CloudTrail securityhub:DisableSecurityHub in the delegated-administrator account, or securityhub:DisassociateFromAdministratorAccount calls from member accounts — peeling members away from the aggregator one at a time.

Query

fields @timestamp, eventName, requestParameters.standardsSubscriptionArns, requestParameters.autoEnable, userIdentity.arn, sourceIPAddress
          | filter eventSource = "securityhub.amazonaws.com" and eventName in ["BatchDisableStandards","UpdateOrganizationConfiguration","DisableSecurityHub","DisassociateFromAdministratorAccount"]
          | sort @timestamp desc
          | limit 100

The CloudWatch Logs Insights query targets all four evasion paths in a single sweep; the standardsSubscriptionArns field carries the disabled standards' ARNs so downstream alert routing can branch on which standard was dropped (CIS vs AFSBP vs PCI vs NIST).

Alert threshold

  • Any BatchDisableStandards targeting CIS or AFSBP in production — page immediately; the org's compliance dashboard depends on at least these two standards and disabling either is a deliberate posture downgrade.
  • UpdateOrganizationConfiguration with autoEnable=false — high-priority ticket within 30 minutes; the change does not affect current members but quietly breaks the org's account-onboarding posture pipeline.
  • DisableSecurityHub in the delegated-admin account — page immediately and treat as confirmed sabotage; the manual flow to re-enable requires re-establishing the org-admin role.

Initial response

  1. Re-enable disabled standards with aws securityhub batch-enable-standards --standards-subscription-requests referencing the IaC-canonical ARN list; confirm via get-enabled-standards read-back before closing the ticket.
  2. Reset autoEnable=true with aws securityhub update-organization-configuration --auto-enable and enumerate any member account created during the disabled-auto-enable window that was not enrolled — these accounts need a one-shot create-members + invite flow.
  3. Open an incident via general/ir.html; pull the Security Hub findings table for the affected window and confirm whether any findings the disabled standards would have generated are now back in the queue post-restoration — coverage gaps surface as a finding-count spike at re-enable time.

References

Equivalent on: Azure · GCP · OCI

aws-log-06-vpc-flow-logs ! MEDIUM DETECTIVE

Enable VPC Flow Logs on every VPC in every account, capturing both ACCEPT and REJECT traffic, with the destination set to a central S3 bucket (KMS-encrypted) rather than CloudWatch Logs for any organisation operating at meaningful scale (Amazon VPC User Guide — Flow Logs (accessed 2026-05)). Flow logs record source/destination IP, source/destination port, protocol, packet count, byte count, start/end timestamps, action (ACCEPT/REJECT), and the AWS-side log status for every IP flow at the ENI level — the network-layer record that fills the gap CloudTrail (which only records API calls) leaves. The S3 destination is preferred at scale because CloudWatch Logs ingestion is roughly 10× more expensive per GB than S3 PUT for the same volume; the operational tradeoff is that S3 destinations need Athena or a SIEM connector for query, while CloudWatch destinations come with Logs Insights built-in. Pick S3 if the organisation already operates Athena or a SIEM that ingests S3; pick CloudWatch only for VPCs small enough that operational simplicity outweighs the bill. Capture both ACCEPT and REJECT — REJECT-only loses the lateral-movement signal where the attacker uses a permitted path; ACCEPT-only loses the perimeter-probing signal where the attacker rattles every closed port looking for an opening.

Remediation — AWS CLI

# Create a VPC Flow Log delivering ALL traffic to a central S3 bucket.
aws ec2 create-flow-logs \
  --resource-type VPC \
  --resource-ids vpc-0abc123def4567890 \
  --traffic-type ALL \
  --log-destination-type s3 \
  --log-destination arn:aws:s3:::org-vpc-flow-logs-111122223333/vpc-0abc123def4567890/

# Audit: ensure every VPC in every region has a flow log.
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  for vpc in $(aws ec2 describe-vpcs --region "$region" --query 'Vpcs[].VpcId' --output text); do
    fl=$(aws ec2 describe-flow-logs --region "$region" \
      --filters Name=resource-id,Values="$vpc" \
      --query 'FlowLogs[].FlowLogId' --output text)
    if [ -z "$fl" ]; then echo "MISSING $region $vpc"; fi
  done
done

Remediation — Terraform

# Terraform AWS provider ~> 5.0
# Source: AWS docs (accessed 2026-05)
resource "aws_s3_bucket" "flow_logs" {
  bucket = "org-vpc-flow-logs-${var.payer_account_id}"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "flow_logs" {
  bucket = aws_s3_bucket.flow_logs.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.logs.arn
    }
  }
}

resource "aws_kms_key" "logs" {
  description         = "VPC flow log encryption CMK"
  enable_key_rotation = true
}

resource "aws_flow_log" "vpc" {
  for_each             = toset(var.vpc_ids)
  vpc_id               = each.value
  traffic_type         = "ALL"
  log_destination_type = "s3"
  log_destination      = "${aws_s3_bucket.flow_logs.arn}/${each.value}/"

  destination_options {
    file_format        = "parquet"
    per_hour_partition = true
  }
}

Remediation — CloudFormation

AWSTemplateFormatVersion: '2010-09-09'
Description: VPC flow logs streamed to a KMS-encrypted CloudWatch Logs group with explicit retention.
Parameters:
  VpcId:
    Type: AWS::EC2::VPC::Id
  FlowLogsRoleArn:
    Type: String
  FlowLogsKmsKeyArn:
    Type: String
Resources:
  FlowLogsGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/vpc/flow-logs/${VpcId}'
      RetentionInDays: 365
      KmsKeyId: !Ref FlowLogsKmsKeyArn
  VpcFlowLog:
    Type: AWS::EC2::FlowLog
    Properties:
      ResourceId: !Ref VpcId
      ResourceType: VPC
      TrafficType: ALL
      LogDestinationType: cloud-watch-logs
      LogGroupName: !Ref FlowLogsGroup
      DeliverLogsPermissionArn: !Ref FlowLogsRoleArn

Compliance mapping

CIS AWS Foundations v3.0.0 CIS Microsoft Azure Foundations v3.0.0 CIS GCP Foundation v4.0.0 CIS OCI Foundation v2.0.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015
3.9n/an/an/a AU-2; AU-12; SC-7A.8.15; A.8.16CLD.12.4.5

Log signals

  • CloudTrail ec2:DeleteFlowLogs — destroys the flow-log resource for a VPC; subsequent VPC traffic flows are unobservable until a new flow-log is created.
  • CloudTrail ec2:CreateFlowLogs events where requestParameters.trafficType is ACCEPT only (rather than ALL) — drops the REJECT half of the flow record, which is the canonical signal for security-group denials and lateral-movement probing.
  • CloudWatch Logs delivery-status metric for the flow-log delivery role failing for more than 10 minutes — flow records may be generated but failing to land in the target log group or S3 bucket, producing a silent coverage gap.

Query

fields @timestamp, eventName, requestParameters.resourceIds, requestParameters.trafficType, requestParameters.logDestinationType, requestParameters.logDestination, userIdentity.arn
          | filter eventSource = "ec2.amazonaws.com" and eventName in ["DeleteFlowLogs","CreateFlowLogs","ModifyFlowLog"]
          | sort @timestamp desc
          | limit 100

Pair the CloudWatch Logs Insights search with a daily completeness check that lists describe-flow-logs per VPC and confirms (a) coverage on every production VPC and (b) trafficType=ALL; the completeness check catches drift that the event-driven query misses if the recorder mutation pre-dated the alert pipeline.

Alert threshold

  • Any DeleteFlowLogs in production — page immediately; the VPC's network forensics surface goes dark at that instant and re-creating the flow-log later does not recover the gap.
  • CreateFlowLogs with trafficType=ACCEPT only — high-priority ticket; the REJECT-side coverage is essential for security-group ingress detection and the operator should be redirected to trafficType=ALL or split flow-logs covering both directions.
  • Per-VPC flow-record ingestion below 5% of 7-day baseline for two consecutive 5-minute windows — informational; promote to incident if the gap persists for 30 minutes (legitimate traffic dips that long are very rare in production VPCs).

Initial response

  1. Re-create the flow-log with aws ec2 create-flow-logs --resource-ids {vpc} --resource-type VPC --traffic-type ALL --log-destination-type cloud-watch-logs --log-group-name {group} --deliver-logs-permission-arn {role-arn}; restore from IaC rather than the captured CloudTrail event when feasible so any drift in the rest of the flow-log surface closes simultaneously.
  2. Inventory the affected VPC's CloudTrail RunInstances, AuthorizeSecurityGroupIngress, and CreateNetworkInterface calls during the coverage gap — the absence of flow records does not erase the CloudTrail timeline and these calls correlate with whatever the operator was trying to hide.
  3. Open an incident via general/ir.html; if any GuardDuty findings reference the VPC and overlap the gap window, treat the gap as the most-likely active-attack window and prioritise the VPC's resource investigation in the IR sequence.

References

Equivalent on: Azure · GCP · OCI

aws-log-07-cloudwatch-alarms-critical ! HIGH DETECTIVE

Define CloudWatch metric filters and alarms on the canonical "things-went-wrong" CloudTrail events: root account login, console login without MFA, IAM policy change, CloudTrail configuration change, KMS customer-managed key disable or scheduled deletion, S3 bucket policy change, NACL change, SG change, route-table change, unauthorized API call (errorCode AccessDenied or UnauthorizedOperation), AWS Config configuration changes, VPC change, network gateway change, STS GetSessionToken / GetFederationToken, and disabling of GuardDuty/Security Hub — the CIS AWS Foundations v3.0.0 §4.1–4.15 list, plus the modern additions (Amazon CloudWatch User Guide — events and metric filters (accessed 2026-05)). These are the events that should never happen during normal business operation; if they do, a human needs to look at them within minutes. Implementation pattern: a CloudWatch log group receives the CloudTrail trail (via the trail's CloudWatch Logs delivery option, or via a Lambda subscription from S3); a aws_cloudwatch_log_metric_filter matches the canonical event pattern; a aws_cloudwatch_metric_alarm triggers on any non-zero metric value over a 1-period evaluation window and routes via SNS to PagerDuty / Opsgenie / email. The CIS benchmark cells fill themselves out cleanly here because §4.1–4.15 specifies the exact event patterns.

Remediation — AWS CLI

# Metric filter: root account login (CIS §4.3 equivalent in the v3.0.0 numbering).
aws logs put-metric-filter \
  --log-group-name /aws/cloudtrail/org-audit-trail \
  --filter-name root-login \
  --filter-pattern '{$.userIdentity.type="Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType!="AwsServiceEvent"}' \
  --metric-transformations \
    metricName=RootAccountLogin,metricNamespace=SecurityAlarms,metricValue=1

# Alarm on any non-zero value of RootAccountLogin.
aws cloudwatch put-metric-alarm \
  --alarm-name root-account-login \
  --metric-name RootAccountLogin --namespace SecurityAlarms \
  --statistic Sum --period 300 --threshold 0 \
  --comparison-operator GreaterThanThreshold \
  --evaluation-periods 1 \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:security-pager

# Equivalent stubs for the rest of the §4.x set (IAM policy change, CT config change,
# KMS key disable, S3 bucket policy change, etc.) follow the same pattern — see
# the Terraform module below for the full list.

Remediation — Terraform

# Terraform AWS provider ~> 5.0
# Source: AWS docs (accessed 2026-05)
locals {
  cis_alarms = {
    root_login = {
      pattern = "{$.userIdentity.type=\"Root\" && $.userIdentity.invokedBy NOT EXISTS && $.eventType!=\"AwsServiceEvent\"}"
      metric  = "RootAccountLogin"
    }
    iam_policy_change = {
      pattern = "{($.eventName=DeleteGroupPolicy)||($.eventName=DeleteRolePolicy)||($.eventName=DeleteUserPolicy)||($.eventName=PutGroupPolicy)||($.eventName=PutRolePolicy)||($.eventName=PutUserPolicy)||($.eventName=CreatePolicy)||($.eventName=DeletePolicy)||($.eventName=CreatePolicyVersion)||($.eventName=DeletePolicyVersion)||($.eventName=AttachRolePolicy)||($.eventName=DetachRolePolicy)||($.eventName=AttachUserPolicy)||($.eventName=DetachUserPolicy)||($.eventName=AttachGroupPolicy)||($.eventName=DetachGroupPolicy)}"
      metric  = "IamPolicyChange"
    }
    cloudtrail_config_change = {
      pattern = "{($.eventName=CreateTrail)||($.eventName=UpdateTrail)||($.eventName=DeleteTrail)||($.eventName=StartLogging)||($.eventName=StopLogging)}"
      metric  = "CloudTrailConfigChange"
    }
    kms_key_disable = {
      pattern = "{($.eventSource=kms.amazonaws.com) && (($.eventName=DisableKey)||($.eventName=ScheduleKeyDeletion))}"
      metric  = "KmsKeyDisable"
    }
    s3_bucket_policy_change = {
      pattern = "{($.eventSource=s3.amazonaws.com) && (($.eventName=PutBucketAcl)||($.eventName=PutBucketPolicy)||($.eventName=PutBucketCors)||($.eventName=PutBucketLifecycle)||($.eventName=PutBucketReplication)||($.eventName=DeleteBucketPolicy)||($.eventName=DeleteBucketCors)||($.eventName=DeleteBucketLifecycle)||($.eventName=DeleteBucketReplication))}"
      metric  = "S3BucketPolicyChange"
    }
  }
}

resource "aws_cloudwatch_log_metric_filter" "cis" {
  for_each       = local.cis_alarms
  name           = each.key
  log_group_name = aws_cloudwatch_log_group.trail.name
  pattern        = each.value.pattern

  metric_transformation {
    name      = each.value.metric
    namespace = "SecurityAlarms"
    value     = "1"
  }
}

resource "aws_cloudwatch_metric_alarm" "cis" {
  for_each            = local.cis_alarms
  alarm_name          = "cis-${each.key}"
  metric_name         = each.value.metric
  namespace           = "SecurityAlarms"
  statistic           = "Sum"
  period              = 300
  threshold           = 0
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  alarm_actions       = [aws_sns_topic.security_pager.arn]
  treat_missing_data  = "notBreaching"
}

Remediation — CloudFormation

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudWatch alarm on root-account console-login event count (zero-tolerance).
Parameters:
  TrailLogGroupName:
    Type: String
  AlarmTopicArn:
    Type: String
Resources:
  RootLoginMetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      LogGroupName: !Ref TrailLogGroupName
      FilterPattern: '{ ($.userIdentity.type = "Root") && ($.eventName = "ConsoleLogin") }'
      MetricTransformations:
        - MetricName: RootConsoleLogins
          MetricNamespace: Security/Identity
          MetricValue: '1'
  RootLoginAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: root-console-login
      MetricName: RootConsoleLogins
      Namespace: Security/Identity
      Statistic: Sum
      Period: 60
      EvaluationPeriods: 1
      Threshold: 0
      ComparisonOperator: GreaterThanThreshold
      TreatMissingData: notBreaching
      AlarmActions:
        - !Ref AlarmTopicArn

Compliance mapping

CIS AWS Foundations v3.0.0 CIS Microsoft Azure Foundations v3.0.0 CIS GCP Foundation v4.0.0 CIS OCI Foundation v2.0.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015
4.1–4.15n/an/an/a AU-6; SI-4A.8.16CLD.12.4.5

Log signals

  • CloudTrail cloudwatch:DeleteAlarms targeting alarms in the canonical critical-alarm naming convention (commonly prefixed security- or tagged severity=critical) — removes the trigger entirely so the metric still emits but no alert fans out.
  • CloudTrail cloudwatch:DisableAlarmActions — leaves the alarm in place visually but suppresses every SNS / Lambda / EventBridge action it would fire; the most evasion-friendly variant because the alarm console still shows the alarm as healthy.
  • CloudTrail logs:DeleteMetricFilter on metric-filters that feed critical alarms (root-login, IAM-policy-change, etc.) — destroys the metric upstream so the alarm transitions to INSUFFICIENT_DATA and is often misinterpreted as benign.

Query

fields @timestamp, eventName, requestParameters.alarmNames, requestParameters.filterName, requestParameters.logGroupName, userIdentity.arn
          | filter eventSource in ["monitoring.amazonaws.com","logs.amazonaws.com"] and eventName in ["DeleteAlarms","DisableAlarmActions","DeleteMetricFilter","PutMetricAlarm"]
          | filter requestParameters.alarmNames.0 like /^security-/ or requestParameters.filterName like /^security-/
          | sort @timestamp desc
          | limit 100

Pair the CloudWatch Logs Insights query with a daily completeness check that lists the canonical alarm names from the IaC repository and asserts each is present with state OK or ALARM (not INSUFFICIENT_DATA) and ActionsEnabled=true.

Alert threshold

  • Any DeleteAlarms or DisableAlarmActions on a critical-severity alarm — page immediately; the inventory of critical alarms is small and the steady-state mutation rate is essentially zero.
  • DeleteMetricFilter on filters whose name matches the security-* naming convention — page within 5 minutes; the alarm fed by the filter will transition to INSUFFICIENT_DATA within minutes and an unattended alarm in that state is the same as no alarm.
  • Critical alarm in INSUFFICIENT_DATA state for more than 60 minutes without a corresponding metric-emission incident — informational; usually a misconfigured filter rather than a delete, but treat as a hygiene ticket.

Initial response

  1. Restore the alarm or metric-filter from IaC: aws cloudwatch put-metric-alarm or aws logs put-metric-filter with the IaC-canonical inputs; confirm the alarm transitions back to OK by triggering a synthetic test event in non-production first.
  2. Inventory CloudTrail events that the deleted alarm would have flagged over the prior 24 hours (root-login, IAM-policy-change, MFA-deactivation, etc.) — the alarm being missing does not erase the underlying CloudTrail events; reconstruct what should have fired.
  3. Open an incident via general/ir.html if any reconstructed event would have been a critical-severity fire; the principal that deleted the alarm and the principal that performed the underlying action are likely related and should be investigated together.

References

Equivalent on: Azure · GCP · OCI

aws-log-08-cloudtrail-lake ! MEDIUM RESPONSIVE

Provision a CloudTrail Lake event data store with at least 7-year retention as the forensic SQL surface over CloudTrail history. CloudTrail Lake (GA 2023) is a managed, immutable, ACID-queryable store for CloudTrail events that lets incident responders run SQL — joins, aggregates, regex filters, time-range scans — against billions of API calls without standing up Athena, partitioning S3 logs, or maintaining a SIEM index of CloudTrail data (AWS CloudTrail User Guide — CloudTrail Lake (accessed 2026-05)). The pairing is critical: aws-ir-04 on the AWS IR page documents the forensic-query workflow (pre-built SQL for "which roles did this principal assume in the last 90 days", "which IPs called sensitive APIs against this account", and so on); this control ships the data store those queries run against. The control is typed RESPONSIVE because the value-add over the raw S3-backed CloudTrail logs is in the post-incident query workflow — Lake does not detect anything by itself, it accelerates the response phase. CIS AWS Foundations v3.0.0 (Jan 2024) post-dates CloudTrail Lake GA but does not codify it as a numbered control, hence the cell reads n/a (post-v3.0.0). Severity MEDIUM because the cheaper-but-slower fallback (Athena over the S3 trail logs) covers the same forensic surface for organisations whose IR cadence does not need sub-second query response.

Remediation — AWS CLI

# Create an organization-wide multi-region event data store with 7-year retention.
aws cloudtrail create-event-data-store \
  --name org-forensic-lake \
  --retention-period 2557 \
  --multi-region-enabled \
  --organization-enabled \
  --advanced-event-selectors '[
    {"Name":"Management events","FieldSelectors":[
      {"Field":"eventCategory","Equals":["Management"]}]}]'

# Run a forensic query.
EDS_ID=<event-data-store-id>
aws cloudtrail start-query --query-statement \
  "SELECT eventTime, eventName, sourceIPAddress
   FROM ${EDS_ID}
   WHERE userIdentity.accessKeyId = 'AKIA...'
   AND eventTime > '2026-01-01T00:00:00Z'
   ORDER BY eventTime"

Remediation — Terraform

# Terraform AWS provider ~> 5.0
# Source: AWS docs (accessed 2026-05)
resource "aws_cloudtrail_event_data_store" "forensic" {
  name                 = "org-forensic-lake"
  retention_period     = 2557   # ~7 years (max).
  multi_region_enabled = true
  organization_enabled = true

  advanced_event_selector {
    name = "Management events"
    field_selector {
      field  = "eventCategory"
      equals = ["Management"]
    }
  }

  # Termination protection on by default — flip to false only to delete.
  termination_protection_enabled = true
}

Remediation — CloudFormation

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudTrail Lake event data store for long-horizon (7-year) management-event analytics.
Resources:
  AnalyticsEventDataStore:
    Type: AWS::CloudTrail::EventDataStore
    Properties:
      Name: cloudtrail-lake-analytics
      MultiRegionEnabled: true
      OrganizationEnabled: true
      RetentionPeriod: 2557
      TerminationProtectionEnabled: true
      AdvancedEventSelectors:
        - Name: Management events
          FieldSelectors:
            - Field: eventCategory
              Equals: [Management]

Compliance mapping

CIS AWS Foundations v3.0.0 CIS Microsoft Azure Foundations v3.0.0 CIS GCP Foundation v4.0.0 CIS OCI Foundation v2.0.0 NIST SP 800-53 rev5 ISO/IEC 27001:2022 ISO/IEC 27017:2015
n/a (post-v3.0.0)n/an/an/a AU-11; IR-4(7)A.8.15; A.5.28CLD.12.4.5

Log signals

  • CloudTrail cloudtrail:DeleteEventDataStore targeting the org's CloudTrail Lake event-data-store — Lake retention is independent of the live trail's S3 bucket and deleting the Lake destroys the long-tail forensic archive that survives trail evasion attempts.
  • CloudTrail cloudtrail:UpdateEventDataStore shortening retentionPeriod (e.g. from 2557 days / 7 years down to 90 days) — silently truncates retained history without destroying the store, and the truncated events are unrecoverable.
  • CloudTrail cloudtrail:StartQuery events against the Lake from principals outside the SOC / IR role allow-list — Lake queries against management-event tables can extract bulk sensitive activity and should be tightly bounded to documented investigation tickets.

Query

fields @timestamp, eventName, requestParameters.eventDataStore, requestParameters.retentionPeriod, requestParameters.queryStatement, userIdentity.arn
          | filter eventSource = "cloudtrail.amazonaws.com" and eventName in ["DeleteEventDataStore","UpdateEventDataStore","StartQuery"]
          | sort @timestamp desc
          | limit 100

The CloudWatch Logs Insights query is the cheapest tier; for the StartQuery noise problem, layer in a second query that filters out the documented SOC role-arns so only out-of-band queriers surface.

Alert threshold

  • Any DeleteEventDataStore in production — page immediately and trigger the IaC restore; the Lake event-data-store cannot be un-deleted after the 7-day pending-deletion window, so the response window is firm.
  • UpdateEventDataStore reducing retentionPeriod below the org's compliance floor (typically 365 days for SOC 2 / 2557 days for HIPAA) — page within 10 minutes; the truncation is irreversible from the moment it commits.
  • StartQuery from a principal outside the SOC / IR allow-list — high-priority ticket within 15 minutes; route to the principal's manager with the query text attached so the business justification is captured at the time of the query rather than reconstructed later.

Initial response

  1. Restore the event-data-store via the IaC stack if the deletion is still within the pending-deletion window (aws cloudtrail restore-event-data-store); otherwise re-create and accept the forensic gap.
  2. For retention shortening, immediately revert via aws cloudtrail update-event-data-store --retention-period {prior-value} and capture the data that the truncation cut, if any is still within the Lake's archival hold.
  3. Open an incident via general/ir.html for the StartQuery case; correlate the query text against the most-recent CloudTrail events the queried filter targeted — the principal is usually trying to scope out what was logged about their own recent activity.

References

Equivalent on: Azure · GCP · OCI · pairs with aws-ir-04 forensic queries

Sources