This check flags AWS accounts that have no CloudWatch alarm watching for IAM policy changes (CIS 3.4). Without it, an attacker or careless engineer can quietly widen permissions and you will never know. Fix it by creating a metric filter on your CloudTrail log group and wiring it to an SNS-backed CloudWatch alarm.
IAM is the front door to your AWS account. Every permission boundary, every trust relationship, and every escalation path runs through it. When someone modifies an IAM policy, whether attaching AdministratorAccess to a compromised user or loosening a resource policy on a critical role, that change should never pass silently. The No Alarm for IAM Policy Changes check verifies that your account has a CloudWatch alarm configured to detect exactly these events, as recommended by CIS AWS Foundations Benchmark control 3.4.
What this check detects
The check looks for a CloudWatch metric filter and associated alarm that monitor your CloudTrail logs for IAM policy mutation events. Specifically, CIS 3.4 expects a metric filter matching API calls such as:
DeleteGroupPolicy,DeleteRolePolicy,DeleteUserPolicyPutGroupPolicy,PutRolePolicy,PutUserPolicyCreatePolicy,DeletePolicy,CreatePolicyVersion,DeletePolicyVersionAttachRolePolicy,DetachRolePolicyAttachUserPolicy,DetachUserPolicyAttachGroupPolicy,DetachGroupPolicy
If no metric filter matches these events, or a filter exists but is not connected to an alarm with a working SNS notification, the check fails.
Note: This check depends on a multi-region CloudTrail trail delivering events to a CloudWatch Logs group. If you have no trail feeding CloudWatch Logs, there is nothing for a metric filter to read, so fix CloudTrail logging first (CIS 3.1) before this one.
Why it matters
Permission changes are the quietest and most dangerous part of an account compromise. An attacker rarely starts with full admin. They land with a foothold, often a leaked access key or an over-permissioned CI role, and then escalate. Attaching a managed policy or editing an inline policy is one of the fastest paths from "limited user" to "owns the account."
Consider a realistic chain:
- A developer commits an access key to a public repo.
- An automated scanner picks it up within minutes.
- The attacker calls
AttachUserPolicyto bindAdministratorAccessto the user tied to that key. - They create new access keys, spin up mining instances, and exfiltrate data.
Step 3 is the moment you want to know about. With a CIS 3.4 alarm in place, that single AttachUserPolicy call pages your on-call within a minute. Without it, you find out when the bill arrives or when a customer reports a breach.
Beyond external attackers, this alarm catches internal mistakes too. An engineer broadening a role policy during a late-night incident, a Terraform run that drifts a trust policy, a contractor granting themselves more access than agreed. These are not malicious, but they erode your security posture, and you want a record and a signal when they happen.
Warning: This alarm is a detective control, not a preventive one. It tells you a change happened, it does not stop it. Pair it with permission boundaries and service control policies if you want to actually block dangerous IAM edits.
How to fix it
The remediation has three parts: a metric filter on your CloudTrail log group, an SNS topic with a subscription, and a CloudWatch alarm linking them. Below is the full sequence using the AWS CLI. Replace the placeholder values with your own.
1. Create an SNS topic and subscribe to it
aws sns create-topic --name cis-iam-policy-changes
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:111122223333:cis-iam-policy-changes \
--protocol email \
--notification-endpoint [email protected]
Confirm the subscription from the email you receive before moving on, otherwise the alarm will fire into the void.
2. Create the metric filter on the CloudTrail log group
aws logs put-metric-filter \
--log-group-name "CloudTrail/DefaultLogGroup" \
--filter-name "CIS-IAMPolicyChanges" \
--filter-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-transformations \
metricName=IAMPolicyChanges,metricNamespace=CISBenchmark,metricValue=1
3. Create the CloudWatch alarm
aws cloudwatch put-metric-alarm \
--alarm-name "CIS-3.4-IAMPolicyChanges" \
--alarm-description "Alarm for IAM policy changes (CIS 3.4)" \
--metric-name IAMPolicyChanges \
--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-iam-policy-changes
Tip: Find your CloudTrail log group name with aws logs describe-log-groups --query "logGroups[].logGroupName". The default is often something like aws-cloudtrail-logs-<account-id>, not the example above.
Doing it in the console
- Open CloudWatch → Log groups and select your CloudTrail log group.
- Choose Metric filters → Create metric filter and paste the filter pattern above.
- Name the metric
IAMPolicyChangesunder namespaceCISBenchmarkwith a value of1. - On the created filter, choose Create alarm, set the threshold to
>= 1over a 5 minute period. - Attach an SNS topic for notifications and confirm the subscription.
Terraform: make it reproducible
Clicking through the console once is fine, but you want this codified so every account gets the same alarm. Here is the equivalent in Terraform.
resource "aws_sns_topic" "iam_policy_changes" {
name = "cis-iam-policy-changes"
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.iam_policy_changes.arn
protocol = "email"
endpoint = "[email protected]"
}
resource "aws_cloudwatch_log_metric_filter" "iam_policy_changes" {
name = "CIS-IAMPolicyChanges"
log_group_name = var.cloudtrail_log_group_name
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_transformation {
name = "IAMPolicyChanges"
namespace = "CISBenchmark"
value = "1"
}
}
resource "aws_cloudwatch_metric_alarm" "iam_policy_changes" {
alarm_name = "CIS-3.4-IAMPolicyChanges"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
metric_name = "IAMPolicyChanges"
namespace = "CISBenchmark"
period = 300
statistic = "Sum"
threshold = 1
treat_missing_data = "notBreaching"
alarm_actions = [aws_sns_topic.iam_policy_changes.arn]
}
Warning: CloudWatch alarms and SNS notifications carry a small cost, and a noisy alarm in a busy account can generate a steady trickle of emails. Route notifications into a dedicated security channel or ticketing system rather than personal inboxes so they do not get ignored.
How to prevent it from happening again
One-off remediation does not scale. The real win is making sure no account ever exists without this alarm.
- Centralize with an organization-wide trail. Use a single multi-region CloudTrail at the organization level so every member account logs to one place. You can then build the CIS 3.x metric filters and alarms against that central log group once.
- Bake it into your landing zone. If you use AWS Control Tower, Account Factory, or a custom Terraform baseline, include the metric filters and alarms in the module that provisions every new account. A new account should arrive fully monitored.
- Gate it in CI/CD. Run a policy-as-code scan on every infrastructure pull request. Tools like Checkov, tfsec, or OPA can assert that the CIS alarm resources exist before a Terraform apply is allowed to merge.
- Continuous detection. Lensix re-runs this check on a schedule, so if someone deletes the alarm or the metric filter drifts, you find out fast rather than at your next audit.
A simple Checkov-style guardrail in your pipeline can confirm the alarm resource is present:
checkov -d ./terraform --check CKV_AWS_111 --compact
Tip: Rather than maintaining 16 separate CIS metric filter alarms by hand, wrap them in a single reusable Terraform module and call it once per account. CIS 3.1 through 3.14 all follow this same metric-filter-plus-alarm pattern, so a loop over a map of event patterns keeps the whole set in one place.
Best practices
- Tune the noise. In environments where IAM changes flow through automated pipelines, you may see frequent legitimate alarms. Do not silence them. Instead, route pipeline-driven changes through a known role and enrich your alerting so human-initiated changes stand out.
- Correlate, do not just alert. Feed these events into a SIEM or Amazon Security Lake so an IAM policy change can be correlated with other suspicious activity, like a new access key created moments earlier.
- Add preventive controls alongside detection. Service control policies can deny attaching
AdministratorAccessoutside a break-glass role, and permission boundaries cap what a role can grant. Detection tells you what happened, prevention limits the blast radius. - Test the path end to end. Perform a benign IAM change in a test account and confirm the alarm fires and the notification lands where it should. An alarm nobody receives is no alarm at all.
- Cover the full CIS monitoring set. IAM policy changes are one of several CIS monitoring controls. Root account usage, console logins without MFA, security group changes, and route table changes all deserve the same treatment.
Danger: Never delete a CIS metric filter or alarm to quiet noise during an incident. Doing so blinds you exactly when visibility matters most, and an attacker who has gained access will often try to remove these alarms first. Treat the alarm resources themselves as critical and monitor for their deletion.
Setting up the IAM policy change alarm takes a few minutes, but it buys you something hard to overstate: a real-time signal the moment someone reaches for the keys to your account. Configure it once, codify it everywhere, and let continuous checks make sure it stays in place.

