This check flags AWS accounts that have no CloudWatch alarm watching for root user activity. Because the root user can do anything in your account with no guardrails, you want to know the instant it logs in. Fix it by creating a metric filter on a multi-region CloudTrail and wiring it to an SNS-backed CloudWatch alarm.
The root user is the original sin of every AWS account. It has unrestricted access to every service, every resource, and every billing control, and it cannot be limited by IAM policies the way a normal user can. CIS Benchmark recommendation 3.3 says you should monitor for any use of it, and this Lensix check (account_alarm_rootusage) tells you when that monitoring is missing.
If your account has root credentials sitting around without an alarm watching them, you have a blind spot exactly where you can least afford one.
What this check detects
The check inspects your account for a CloudWatch alarm tied to a CloudTrail metric filter that matches root account sign-in and API activity. Specifically, it looks for three things working together:
- A CloudTrail trail that is logging management events across all regions
- A CloudWatch Logs metric filter that matches events where the caller identity is the root user
- A CloudWatch alarm built on that metric filter, attached to an SNS topic with at least one subscriber
If any link in that chain is missing, the check fails. A metric filter with no alarm is useless. An alarm with no SNS subscription is useless. Lensix verifies the full path from event to notification.
Note: Root usage refers to the AWS account root user, the identity tied to the email address you signed up with. It is separate from any IAM user, even one named "admin" or "root". You can tell root activity in CloudTrail because the userIdentity.type field is set to Root.
Why it matters
The root user bypasses the safety nets you build everywhere else. Service control policies, permission boundaries, and IAM deny statements do not constrain it. That makes any root login a high-signal event, and in a well-run account it should be rare to the point of being suspicious.
Consider what an attacker can do with root credentials that they cannot do with a compromised IAM key:
- Change the account's contact email and recovery options, locking you out
- Close the account or modify billing
- Delete CloudTrail trails and disable GuardDuty, then cover their tracks
- Restore deleted resources or remove MFA requirements set by SCPs
Root credentials are also a favorite target because people reuse them. The email and password were often set during account creation, sometimes stored in a shared password manager or a wiki page that predates your security program. If those credentials leak and there is no alarm, the first sign of trouble might be a surprise on your next AWS invoice or a ransom note.
The goal is not to make root usage impossible. Some operations, like changing the support plan or recovering a deleted account, genuinely require it. The goal is to make sure no root login happens without a human noticing.
From a compliance angle, CIS 3.3 is referenced by PCI DSS, SOC 2, and FedRAMP control mappings. Auditors look for this alarm specifically, so a failing check here can show up directly in a report.
How to fix it
You need a multi-region CloudTrail trail feeding into CloudWatch Logs, a metric filter, and an alarm. The steps below assume you already have a trail. If you do not, the trail comes first.
Step 1: Confirm you have a CloudTrail trail sending to CloudWatch Logs
aws cloudtrail describe-trails \
--query 'trailList[?IsMultiRegionTrail==`true`].[Name,CloudWatchLogsLogGroupArn]' \
--output table
If the CloudWatchLogsLogGroupArn column is empty, the trail is writing to S3 only and you need to point it at a log group before alarms will work.
Warning: Sending CloudTrail events to CloudWatch Logs incurs ingestion and storage charges. For root monitoring alone the volume is tiny, but if you have other high-volume trails sharing a log group, watch your CloudWatch bill after enabling this.
Step 2: Create the metric filter
This filter matches any event where the caller is the root user, while ignoring the routine service-linked AWS console refreshes that can otherwise create noise.
aws logs put-metric-filter \
--log-group-name "/aws/cloudtrail/management-events" \
--filter-name "RootAccountUsage" \
--filter-pattern '{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }' \
--metric-transformations \
metricName=RootAccountUsageCount,metricNamespace=CISBenchmark,metricValue=1,defaultValue=0
Replace the log group name with the one returned in step 1.
Step 3: Create an SNS topic and subscribe to it
TOPIC_ARN=$(aws sns create-topic --name root-usage-alerts --query TopicArn --output text)
aws sns subscribe \
--topic-arn "$TOPIC_ARN" \
--protocol email \
--notification-endpoint [email protected]
Confirm the subscription from the email you receive. Without a confirmed subscriber, the alarm fires into the void and the Lensix check will still consider monitoring incomplete.
Step 4: Create the CloudWatch alarm
aws cloudwatch put-metric-alarm \
--alarm-name "RootAccountUsageAlarm" \
--alarm-description "Alerts on any AWS root account activity (CIS 3.3)" \
--namespace "CISBenchmark" \
--metric-name "RootAccountUsageCount" \
--statistic Sum \
--period 300 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1 \
--treat-missing-data notBreaching \
--alarm-actions "$TOPIC_ARN"
Once this is in place, re-run the check. The chain from CloudTrail event to confirmed SNS subscriber is now complete.
Tip: Send the SNS topic to a channel humans actually read. An email alias that nobody monitors is technically compliant but practically useless. Pipe it to PagerDuty, Opsgenie, or a Slack channel through an AWS Chatbot integration so the alert lands where your on-call engineer will see it at 3 a.m.
Doing it as code (Terraform)
Clicking through this once per account does not scale. Here is the same setup in Terraform, which you can drop into a baseline module applied to every account.
resource "aws_sns_topic" "root_usage" {
name = "root-usage-alerts"
}
resource "aws_sns_topic_subscription" "root_usage_email" {
topic_arn = aws_sns_topic.root_usage.arn
protocol = "email"
endpoint = "[email protected]"
}
resource "aws_cloudwatch_log_metric_filter" "root_usage" {
name = "RootAccountUsage"
log_group_name = "/aws/cloudtrail/management-events"
pattern = "{ $.userIdentity.type = \"Root\" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != \"AwsServiceEvent\" }"
metric_transformation {
name = "RootAccountUsageCount"
namespace = "CISBenchmark"
value = "1"
}
}
resource "aws_cloudwatch_metric_alarm" "root_usage" {
alarm_name = "RootAccountUsageAlarm"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
metric_name = "RootAccountUsageCount"
namespace = "CISBenchmark"
period = 300
statistic = "Sum"
threshold = 1
treat_missing_data = "notBreaching"
alarm_actions = [aws_sns_topic.root_usage.arn]
}
How to prevent it from happening again
A one-time fix drifts. New accounts get created, someone deletes the alarm during a cleanup, or a region gets spun up without the trail. The durable answer is to bake this into account provisioning and then enforce it continuously.
- Centralize the trail. Use an organization trail managed from your management or delegated security account so member accounts cannot disable or modify it. This removes a whole class of drift.
- Make it part of the account baseline. Whether you use AWS Control Tower, Account Factory for Terraform, or your own landing zone module, the root alarm should ship with every new account on day zero.
- Gate it in CI/CD. Run a policy-as-code check against your Terraform plan before apply. Tools like Checkov, OPA, or tfsec can assert that the metric filter and alarm exist.
A simple OPA Rego guardrail might look like this:
package terraform.cis
deny[msg] {
not has_root_alarm
msg := "CIS 3.3: no CloudWatch alarm for root account usage found in plan"
}
has_root_alarm {
some r
input.resource_changes[r].type == "aws_cloudwatch_metric_alarm"
contains(input.resource_changes[r].change.after.metric_name, "RootAccountUsage")
}
Tip: Lensix already runs account_alarm_rootusage on a schedule across your accounts, so even if a guardrail is bypassed or someone deletes the alarm manually, you get told about the gap rather than discovering it during an audit.
Best practices
The alarm is a detective control. Pair it with preventive controls so root usage is both rare and visible.
- Lock down root entirely. Enable a hardware or virtual MFA device on the root user, remove any root access keys, and store the password in a break-glass vault that requires approval to access.
- Delete root access keys. There is almost no legitimate reason for the root user to have programmatic keys. Run
aws iam get-account-summaryand confirmAccountAccessKeysPresentis0. - Restrict root with SCPs. In AWS Organizations, you can use service control policies to deny most root actions across member accounts, leaving only the operations that genuinely require it.
- Test the alert path. Once a quarter, deliberately log in as root in a non-production account and confirm the alert actually reaches a human. An untested alarm is a guess.
- Bundle the other CIS monitoring alarms. Recommendation 3.3 is one of around a dozen CIS monitoring alarms. Unauthorized API calls, console sign-in without MFA, and IAM policy changes all follow the same metric-filter-plus-alarm pattern. Deploy them together as one module rather than piecemeal.
Danger: Never remove or disable the organization CloudTrail trail to silence noise. Doing so blinds every alarm built on top of it, including this one, and an attacker who gets root will do exactly that. If you need to reduce noise, tune the metric filter pattern, not the trail.
Root account monitoring is one of the cheapest, highest-value controls in AWS. A few resources, a confirmed SNS subscriber, and you turn the most dangerous identity in your account from a silent risk into a loud, visible event. Get the alarm in place, push it into your account baseline, and let Lensix keep watch for the day it quietly disappears.

