Back to blog
AWSCloud SecurityIdentity & AccessMonitoring & LoggingOperations & Compliance

No Alarm for Console Login Without MFA (CIS 3.2)

Learn how to detect AWS console sign-ins without MFA using CloudWatch alarms (CIS 3.2), why it matters, and how to fix and automate it with CLI and Terraform.

TL;DR

This check flags AWS accounts that have no CloudWatch alarm watching for console sign-ins that skip MFA. Without it, an attacker using stolen credentials on an unprotected user can log in unnoticed. Fix it by wiring a metric filter on your CloudTrail log group to a CloudWatch alarm that publishes to SNS.

A console login without MFA is one of the loudest warning signs in an AWS account. Either someone has an IAM user that should never have existed without a second factor, or an attacker has working credentials and is walking straight through the front door. CIS AWS Foundations Benchmark control 3.2 exists precisely so you find out the moment it happens, instead of weeks later when the bill or the data leak shows up.

The Lensix check account_alarm_nonmfalogin verifies that you actually have the monitoring in place: a CloudWatch metric filter plus an alarm that fires on ConsoleLogin events where MFA was not used.


What this check detects

The check looks for a complete detection pipeline in your account:

  1. A CloudTrail trail that is delivering management events to a CloudWatch Logs group.
  2. A metric filter on that log group matching console sign-ins where additionalEventData.MFAUsed is No.
  3. A CloudWatch alarm tied to that metric filter.
  4. An SNS topic (or equivalent action) the alarm publishes to, 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 a tree falling in an empty forest.

Note: CloudTrail records the ConsoleLogin event in the signin.amazonaws.com event source. The field additionalEventData.MFAUsed tells you whether a second factor was presented. Root account logins and IAM user logins without MFA both surface here.


Why it matters

Credential theft is still one of the most common ways AWS accounts get compromised. Phished passwords, leaked access keys reused as console passwords, credentials pulled from a developer's machine: all of these end in a login attempt. MFA is the control that stops a stolen password from being enough. The alarm is the control that tells you when that control was not in play.

Consider a realistic sequence:

  • A contractor's IAM user was created months ago for a quick task and never got MFA enforced.
  • Their laptop is compromised and the password lands in an infostealer log sold on a forum.
  • An attacker logs into your console as that user from a residential proxy in another country.
  • They enumerate S3 buckets, create new access keys, and start exfiltrating data.

With this alarm in place, step three generates an alert in minutes. Your on-call engineer sees a non-MFA console login, recognizes it as abnormal, disables the user, and rotates credentials before any real damage happens. Without it, the first signal you get is a GuardDuty finding days later, or a support ticket about a surprise EC2 bill.

Warning: This alarm tells you a login happened without MFA. It does not prevent the login. Detection and prevention are separate jobs. Pair this alarm with an SCP or IAM policy that denies actions when MFA is absent, so you are not relying on alerts alone.


How to fix it

You need a CloudTrail trail already shipping to CloudWatch Logs. If you do not have that yet, set it up first, since the metric filter reads from the log group, not from CloudTrail directly.

Step 1: Create the SNS topic and subscribe to it

aws sns create-topic --name cis-security-alerts

aws sns subscribe \
  --topic-arn arn:aws:sns:us-east-1:111122223333:cis-security-alerts \
  --protocol email \
  --notification-endpoint [email protected]

Confirm the subscription from the email you receive. An unconfirmed subscription will silently drop notifications.

Step 2: Create the metric filter on your CloudTrail log group

aws logs put-metric-filter \
  --log-group-name /aws/cloudtrail/management-events \
  --filter-name ConsoleSigninWithoutMFA \
  --filter-pattern '{ ($.eventName = "ConsoleLogin") && ($.additionalEventData.MFAUsed != "Yes") }' \
  --metric-transformations \
      metricName=ConsoleSigninWithoutMFACount,metricNamespace=CISBenchmark,metricValue=1,defaultValue=0

Note: The pattern uses != "Yes" rather than = "No". Some federated and SSO logins report MFA differently, so matching anything that is not an explicit "Yes" is the safer net per the CIS guidance. Adjust if you intentionally use IAM Identity Center, which logs through a separate event path.

Step 3: Create the alarm

aws cloudwatch put-metric-alarm \
  --alarm-name ConsoleSigninWithoutMFA \
  --alarm-description "Alerts on console logins that did not use MFA (CIS 3.2)" \
  --metric-name ConsoleSigninWithoutMFACount \
  --namespace CISBenchmark \
  --statistic Sum \
  --period 300 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 1 \
  --treat-missing-data notBreaching \
  --alarm-actions arn:aws:sns:us-east-1:111122223333:cis-security-alerts

That is the full chain. A non-MFA login now writes a data point, the alarm crosses its threshold within one five-minute period, and SNS delivers the alert.

Terraform version

If you manage infrastructure as code, define the whole thing declaratively so it cannot drift away again:

resource "aws_sns_topic" "security_alerts" {
  name = "cis-security-alerts"
}

resource "aws_sns_topic_subscription" "email" {
  topic_arn = aws_sns_topic.security_alerts.arn
  protocol  = "email"
  endpoint  = "[email protected]"
}

resource "aws_cloudwatch_log_metric_filter" "no_mfa_login" {
  name           = "ConsoleSigninWithoutMFA"
  log_group_name = "/aws/cloudtrail/management-events"
  pattern        = "{ ($.eventName = \"ConsoleLogin\") && ($.additionalEventData.MFAUsed != \"Yes\") }"

  metric_transformation {
    name          = "ConsoleSigninWithoutMFACount"
    namespace     = "CISBenchmark"
    value         = "1"
    default_value = "0"
  }
}

resource "aws_cloudwatch_metric_alarm" "no_mfa_login" {
  alarm_name          = "ConsoleSigninWithoutMFA"
  alarm_description   = "Alerts on console logins without MFA (CIS 3.2)"
  namespace           = "CISBenchmark"
  metric_name         = aws_cloudwatch_log_metric_filter.no_mfa_login.metric_transformation[0].name
  statistic           = "Sum"
  period              = 300
  threshold           = 1
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  treat_missing_data  = "notBreaching"
  alarm_actions       = [aws_sns_topic.security_alerts.arn]
}

Tip: If you run more than a handful of accounts, build this once as a Terraform module or CloudFormation StackSet and deploy it across the whole organization. CIS 3.x controls all follow the same metric-filter-plus-alarm shape, so a single module can cover sign-in monitoring, root usage, IAM policy changes, and the rest in one pass.


How to prevent it from happening again

Manual fixes rot. Someone deletes a log group during a refactor, a new account gets provisioned without the baseline, and the alarm quietly disappears. Lock it down with automation:

  • Bake it into your account baseline. Whether you use Control Tower, an account factory, or a bootstrap Terraform stack, the CIS alarms should ship with every new account before any workload lands.
  • Gate it in CI/CD. Run a policy check against your Terraform plan so a pull request that removes the log group, filter, or alarm gets blocked. Tools like Checkov, OPA/Conftest, or tfsec can enforce this.
  • Detect drift continuously. Let Lensix re-run account_alarm_nonmfalogin on a schedule so if the resource is deleted out of band you find out the same day, not at audit time.

An example Conftest policy that fails a plan missing the metric filter:

package main

deny[msg] {
  not has_no_mfa_filter
  msg := "Missing CloudWatch metric filter for console login without MFA (CIS 3.2)"
}

has_no_mfa_filter {
  some r
  input.resource.aws_cloudwatch_log_metric_filter[r].name == "ConsoleSigninWithoutMFA"
}

Best practices

  • Enforce MFA, do not just watch for its absence. Attach a policy that denies all actions when aws:MultiFactorAuthPresent is false, and require MFA on every human IAM user. The alarm becomes a backstop rather than your only line of defense.
  • Centralize alerts. Send the SNS topic to a security channel (PagerDuty, Slack via a Lambda, or your SIEM) rather than a single inbox. Email subscriptions get filtered, ignored, or lost when people leave.
  • Move humans to IAM Identity Center. Long-lived IAM users are the usual culprits for non-MFA logins. Federated SSO with enforced MFA removes the problem class, though you should keep the alarm for the root account and any break-glass users.
  • Watch the root account separately. Root should almost never log in. Pair this check with CIS 1.1 and 3.3 monitoring so any root activity, MFA or not, raises a flag.
  • Test the pipeline. Once a quarter, intentionally trigger a benign matching event or use a synthetic log entry to confirm the alert actually reaches a human. An alarm nobody has tested is a liability.

Danger: Do not disable or delete this alarm to silence noise during an incident or migration. If non-MFA logins are firing repeatedly, the answer is to fix the underlying accounts, not to mute the only signal that would warn you of an active compromise.

Getting this check to pass takes a few minutes. Treat it as the entry point to a fuller CIS monitoring baseline, since the same pattern covers the rest of the CIS 3.x controls and gives you broad visibility into account-level changes for very little ongoing cost.