Back to Checks
Account has a CloudWatch Logs metric filter and alarm for root account usage
cloudwatch_log_metric_filter_root_usage
Service: cloudwatchSeverity: medium
OfficialBuilt-inVerified
Categories:logging
Resource Type:AwsCloudWatchAlarm
CloudTrail logs in CloudWatch include a metric filter for root account activity ({ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }) and a linked CloudWatch alarm that triggers when the filter matches.
Risk
Without alerting on root activity, full-privilege actions can proceed unnoticed, impacting:
- confidentiality via data access/exfiltration
- integrity via policy/config tampering
- availability via deletions or shutdowns Delayed detection increases blast radius and persistence.
Run this check with Prowler
Run in Prowler CloudRun
prowler aws --checks cloudwatch_log_metric_filter_root_usageRecommendation
Enable real-time alerts for root activity using a log metric filter and a high-priority alarm with notifications.
Reduce exposure: enforce least privilege, keep root for break-glass with MFA, disable root access keys, and route alerts into incident response for defense in depth.
Remediation
Terraform
# CloudWatch Logs metric filter for root account usage
resource "aws_cloudwatch_log_metric_filter" "<example_resource_name>" {
name = "<example_resource_name>"
log_group_name = "<example_resource_name>"
pattern = "{ $.userIdentity.type = \"Root\" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != \"AwsServiceEvent\" }" # CRITICAL: detects root user actions
metric_transformation {
name = "<example_resource_name>" # CRITICAL: metric used by the alarm
namespace = "<example_resource_name>"
value = "1"
}
}
# Alarm on the root usage metric
resource "aws_cloudwatch_metric_alarm" "<example_resource_name>" {
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
metric_name = "<example_resource_name>" # CRITICAL: matches metric filter
namespace = "<example_resource_name>"
period = 300
statistic = "Sum"
threshold = 1
}NativeIaC
# CloudFormation: Create metric filter and alarm for root account usage
Resources:
RootUsageMetricFilter:
Type: AWS::Logs::MetricFilter
Properties:
LogGroupName: "<example_resource_name>"
FilterPattern: '{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }' # CRITICAL: detects root user actions not invoked by services
MetricTransformations:
- MetricValue: "1"
MetricNamespace: "<example_resource_name>" # CRITICAL: metric namespace used by the alarm
MetricName: "<example_resource_name>" # CRITICAL: metric name used by the alarm
RootUsageAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
EvaluationPeriods: 1
MetricName: "<example_resource_name>" # CRITICAL: alarms on the metric created by the filter
Namespace: "<example_resource_name>"
Period: 300
Statistic: Sum
Threshold: 1Other
1In the AWS console, open CloudWatch > Logs > Log groups and select the CloudTrail log group
2Go to Metric filters > Create metric filter
3For Filter pattern, enter: { $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }
4Click Next, set any Filter name, set Metric namespace and Metric name, set Metric value to 1, then Create metric filter
5Select the new metric filter and click Create alarm
6Set Period to 5 minutes, Statistic to Sum, Threshold type Static with value 1, Evaluation periods 1, then Create alarm
Check Code
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.cloudtrail.cloudtrail_client import (
cloudtrail_client,
)
from prowler.providers.aws.services.cloudwatch.cloudwatch_client import (
cloudwatch_client,
)
from prowler.providers.aws.services.cloudwatch.lib.metric_filters import (
check_cloudwatch_log_metric_filter,
)
from prowler.providers.aws.services.cloudwatch.logs_client import logs_client
class cloudwatch_log_metric_filter_root_usage(Check):
"""Account has a CloudWatch Logs metric filter and alarm for root account usage.
Looks for a CloudWatch Logs metric filter matching the expected pattern on a
log group used by a CloudTrail trail, with at least one alarm on its metric.
- PASS: A matching metric filter with an associated alarm exists.
- FAIL: No matching metric filter, or a filter without an alarm, was found.
- MANUAL: CloudTrail trails, log groups, metric filters or alarms could
not be listed in at least one region, so the absence of a filter/alarm
cannot be asserted.
"""
def execute(self) -> list[Check_Report_AWS]:
"""Evaluate the metric filter and alarm coverage for the account.
Returns:
list[Check_Report_AWS]: A single report for the account.
"""
pattern = r"\$\.userIdentity\.type\s*=\s*.?Root.+\$\.userIdentity\.invokedBy NOT EXISTS.+\$\.eventType\s*!=\s*.?AwsServiceEvent.?"
findings = []
report = check_cloudwatch_log_metric_filter(
pattern,
cloudtrail_client.trails,
logs_client.metric_filters,
cloudwatch_client.metric_alarms,
self.metadata(),
)
inventory_unavailable = (
cloudtrail_client.trails_unavailable
or logs_client.log_groups_unavailable
or logs_client.metric_filters_unavailable
or cloudwatch_client.metric_alarms_unavailable
)
if report is None:
report = Check_Report_AWS(metadata=self.metadata(), resource={})
report.status = "FAIL"
report.status_extended = "No CloudWatch log groups found with metric filters or alarms associated."
report.region = logs_client.region
report.resource_id = logs_client.audited_account
report.resource_arn = logs_client.log_group_arn_template
report.resource_tags = []
# A denied listing in any region means the inventory is incomplete: a
# PASS is still backed by a real filter and alarm, but a FAIL (nothing
# found, or a filter found without its alarm) cannot be trusted.
if report.status == "FAIL" and inventory_unavailable:
report.status = "MANUAL"
report.status_extended = "Cannot evaluate CloudWatch metric filters and alarms: CloudTrail trails, log groups, metric filters or alarms could not be listed in at least one region. Verify that the scanning credentials are allowed to call cloudtrail:DescribeTrails, logs:DescribeLogGroups, logs:DescribeMetricFilters and cloudwatch:DescribeAlarms."
findings.append(report)
return findingsReferences
- https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html
- https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/aws/CloudWatchLogs/root-account-usage-alarm.html
- https://asecure.cloud/a/root_account_login/
- https://support.icompaas.com/support/solutions/articles/62000083624-ensure-a-log-metric-filter-and-alarm-exist-for-usage-of-root-account
- https://www.intelligentdiscovery.io/controls/cloudwatch/cloudwatch-alarm-root-account-usage
- https://aws.amazon.com/blogs/security/how-to-receive-notifications-when-your-aws-accounts-root-access-keys-are-used/
- https://www.tenable.com/audits/items/CIS_Amazon_Web_Services_Foundations_v1.5.0_L1.audit:000adfb028a1475075a6b5d2117f53f4