Back to blog
AWSBest PracticesCloud SecurityMonitoring & LoggingOperations & Compliance

No Alarm for AWS Organizations Changes (CIS 3.15)

Learn how to detect and fix missing CloudWatch alarms for AWS Organizations changes (CIS 3.15), with CLI and Terraform remediation steps.

TL;DR

This check flags AWS accounts that have no CloudWatch alarm watching for AWS Organizations changes. Without it, an attacker (or a careless admin) could detach accounts, change SCPs, or invite rogue accounts without anyone noticing. Fix it by routing Organizations API events from CloudTrail to a CloudWatch metric filter and wiring an alarm to SNS.

AWS Organizations is the control plane for your entire multi-account setup. It governs which accounts exist, what service control policies (SCPs) apply to them, and who can do what across the whole org. That makes it a high-value target and a high-blast-radius surface. CIS AWS Foundations Benchmark control 3.15 asks a simple question: if someone changes your Organizations configuration, will you know about it?

This Lensix check, account_alarm_orgchanges, answers that question by verifying you have a CloudWatch alarm wired to detect AWS Organizations changes. If the alarm is missing, the check fails.


What this check detects

The check looks for a complete monitoring pipeline that turns Organizations API activity into an alert. Specifically, it verifies that:

  • A multi-region CloudTrail is delivering logs to CloudWatch Logs.
  • A metric filter exists that matches AWS Organizations API calls (CreateAccount, DeleteOrganization, RemoveAccountFromOrganization, AttachPolicy, DetachPolicy, and similar).
  • A CloudWatch alarm is attached to that metric filter.
  • The alarm has at least one notification action (typically an SNS topic with a real subscriber).

If any link in that chain is broken, the alarm cannot fire, and the check reports a failure. A common false sense of security is having the metric filter but no alarm, or an alarm pointed at an SNS topic that nobody is subscribed to.

Note: CloudWatch alarms can only evaluate metrics, not raw log events. The metric filter is what bridges the gap: it scans CloudTrail log events flowing into CloudWatch Logs and increments a custom metric whenever a matching event appears. The alarm then watches that metric.


Why it matters

Changes to AWS Organizations are rare under normal operation but devastating when malicious. A few concrete scenarios:

Escaping guardrails

SCPs are often the only thing stopping a compromised account from spinning up expensive resources, disabling logging, or operating in unapproved regions. An attacker who gains organization-level permissions can DetachPolicy to strip an SCP off an account, then act freely. Without an alarm, the detachment is buried in CloudTrail and likely goes unread until the damage shows up on a bill or in an incident.

Account exfiltration

The RemoveAccountFromOrganization call pulls a member account out of your org. Once removed, the account no longer inherits SCPs, no longer reports to your consolidated billing, and may fall outside your centralized logging and monitoring. This is a quiet way to carve out a piece of your environment.

Rogue account injection

An attacker with the right permissions can InviteAccountToOrganization or CreateAccount to stand up new accounts inside your org. New accounts can be used for crypto mining, data staging, or as a foothold that blends in with legitimate accounts.

Warning: Organizations API calls only land in the CloudTrail of the management account, in the home region of the organization. If your trail is single-region or scoped to a member account, you will never see these events. This is a common reason the check fails even when teams think they have monitoring.

The business impact is straightforward: Organizations is your blast-radius boundary. Losing visibility into changes there means you can lose visibility into everything downstream.


How to fix it

The remediation builds the full pipeline: CloudTrail to CloudWatch Logs, a metric filter, a metric, an alarm, and an SNS notification. Run these from the management account in your organization's home region.

Step 1: Confirm CloudTrail is sending to CloudWatch Logs

You need a multi-region trail delivering to a CloudWatch Logs group. Check what you have:

aws cloudtrail describe-trails \
  --query 'trailList[].{Name:Name,Multiregion:IsMultiRegionTrail,LogGroup:CloudWatchLogsLogGroupArn}' \
  --output table

If LogGroup is empty for your trail, attach a log group before continuing. The trail must also have a CloudWatch Logs role with permission to write to the group.

Step 2: Create an SNS topic and subscribe to it

TOPIC_ARN=$(aws sns create-topic --name org-changes-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. An alarm pointed at a topic with no confirmed subscribers will pass an API check but never actually notify a human.

Step 3: Create the metric filter

This filter matches the core Organizations write operations. Replace CloudTrail/Logs with your actual log group name.

aws logs put-metric-filter \
  --log-group-name "CloudTrail/Logs" \
  --filter-name OrganizationsChanges \
  --filter-pattern '{ ($.eventSource = organizations.amazonaws.com) && (($.eventName = "AcceptHandshake") || ($.eventName = "AttachPolicy") || ($.eventName = "CreateAccount") || ($.eventName = "CreateOrganizationalUnit") || ($.eventName = "CreatePolicy") || ($.eventName = "DeclineHandshake") || ($.eventName = "DeleteOrganization") || ($.eventName = "DeleteOrganizationalUnit") || ($.eventName = "DeletePolicy") || ($.eventName = "DetachPolicy") || ($.eventName = "DisablePolicyType") || ($.eventName = "EnablePolicyType") || ($.eventName = "InviteAccountToOrganization") || ($.eventName = "LeaveOrganization") || ($.eventName = "RemoveAccountFromOrganization") || ($.eventName = "UpdatePolicy") || ($.eventName = "UpdateOrganizationalUnit")) }' \
  --metric-transformations \
      metricName=OrganizationsChangeCount,metricNamespace=CISBenchmark,metricValue=1,defaultValue=0

Step 4: Create the alarm

aws cloudwatch put-metric-alarm \
  --alarm-name CIS-3.15-OrganizationsChanges \
  --alarm-description "Alerts on AWS Organizations changes (CIS 3.15)" \
  --metric-name OrganizationsChangeCount \
  --namespace CISBenchmark \
  --statistic Sum \
  --period 300 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 1 \
  --treat-missing-data notBreaching \
  --alarm-actions "$TOPIC_ARN"

Tip: Use --treat-missing-data notBreaching so the alarm stays in an OK state when no Organizations changes occur. Without it, a metric that rarely reports data can sit in INSUFFICIENT_DATA and create noise.

Terraform version

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

resource "aws_sns_topic" "org_changes" {
  name = "org-changes-alerts"
}

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

resource "aws_cloudwatch_log_metric_filter" "org_changes" {
  name           = "OrganizationsChanges"
  log_group_name = "CloudTrail/Logs"

  pattern = "{ ($.eventSource = organizations.amazonaws.com) && (($.eventName = \"AttachPolicy\") || ($.eventName = \"DetachPolicy\") || ($.eventName = \"CreateAccount\") || ($.eventName = \"RemoveAccountFromOrganization\") || ($.eventName = \"InviteAccountToOrganization\") || ($.eventName = \"DeleteOrganization\") || ($.eventName = \"UpdatePolicy\")) }"

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

resource "aws_cloudwatch_metric_alarm" "org_changes" {
  alarm_name          = "CIS-3.15-OrganizationsChanges"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  metric_name         = aws_cloudwatch_log_metric_filter.org_changes.metric_transformation[0].name
  namespace           = "CISBenchmark"
  period              = 300
  statistic           = "Sum"
  threshold           = 1
  treat_missing_data  = "notBreaching"
  alarm_actions       = [aws_sns_topic.org_changes.arn]
}

Note: The Terraform pattern above is trimmed to the highest-signal events to keep it readable. Expand it to match the full CLI filter if your compliance scope requires every Organizations write operation covered.


How to prevent it from coming back

Manual fixes drift. Someone deletes a metric filter during a cleanup, a new region gets added, or a trail gets reconfigured. Bake the control into your delivery pipeline so the gap cannot reopen silently.

Manage it as code, centrally

Define this alarm in the same Terraform or CloudFormation stack that provisions your management account baseline. Apply it through CI/CD, not by hand. If the alarm lives in code, a terraform plan will surface any drift before it reaches production.

Gate pull requests with policy-as-code

Use a tool like Checkov, tfsec, or OPA/Conftest to fail builds that remove or weaken the alarm. A simple Conftest policy can assert that an aws_cloudwatch_metric_alarm with the Organizations metric exists in any plan touching the management account:

conftest test plan.json --policy policy/org-alarm.rego

Continuous scanning

Policy-as-code only catches what flows through your pipeline. Out-of-band changes, console edits, and broken SNS subscriptions still slip through. Run Lensix on a schedule against the management account so account_alarm_orgchanges re-evaluates the live state and reopens a finding the moment the alarm goes missing or its topic loses subscribers.

Tip: Deploy this control once in your management account using a CloudFormation StackSet or an organization-level Terraform module, then reference it as the canonical baseline. New SCP and account changes will then always be observed from the one place that actually receives the events.


Best practices

  • Send alerts somewhere humans read. Route the SNS topic to a paging tool or a monitored Slack/Teams channel, not just an inbox nobody checks. Organizations changes are low-frequency and high-importance, exactly the kind of alert that deserves attention.
  • Pair detection with prevention. Lock down who can call Organizations APIs using IAM permission boundaries and SCPs. The alarm tells you when something happened; tight IAM stops most of it from happening at all.
  • Monitor the management account specifically. Organizations events surface only there. Treat the management account as a tier-zero environment with its own dedicated, multi-region trail and alarm set.
  • Cover the whole CIS monitoring family. Control 3.15 is one of a series of CloudWatch alarm checks (root usage, IAM policy changes, CloudTrail config changes, and more). Build them as a single reusable module so they stay consistent.
  • Test the pipeline end to end. Make a benign change, such as creating and deleting a test OU, and confirm the alarm fires and the notification arrives. An untested alarm is an assumption, not a control.

Danger: Do not test this by calling destructive operations like DeleteOrganization or RemoveAccountFromOrganization on real resources. Use CreateOrganizationalUnit followed by DeleteOrganizationalUnit on a throwaway OU to verify the alarm without risking your actual account structure.

Organizations sits at the top of your AWS hierarchy, and changes there ripple across every account you run. A single CloudWatch alarm is cheap insurance against losing sight of the one place you most need to watch.

Alarm for AWS Organizations Changes (CIS 3.15) | Lensix