How to Use AWS Config to Detect Non-Compliant Security Group Rules

Someone on your team opens port 22 to 0.0.0.0/0 on a production security group — maybe for a quick debug session, maybe by accident. Without automated detection, that change sits unnoticed until your next manual audit or, worse, until something goes wrong. AWS Config gives you a managed rule that flags this exact pattern and, wired to SNS, can alert your team within minutes of the change landing.

TL;DR: AWS Config SSH Compliance Detection

StepWhat You're DoingWhy It Matters
1Enable AWS Config with EC2 recordingConfig can't evaluate what it doesn't record
2Deploy restricted-ssh managed ruleAWS-managed logic, no Lambda required
3Create SNS topic + subscriptionHumans need to know when compliance breaks
4Wire EventBridge rule to SNSConfig compliance change events route through EventBridge
5Verify end-to-end with a test SGConfirm the pipeline before you trust it in production

How AWS Config Compliance Detection Works

AWS Config continuously records configuration changes to supported AWS resources. When a security group rule changes, Config captures the new configuration state and evaluates it against any rules you've associated with that resource type. The evaluation result — COMPLIANT or NON_COMPLIANT — is stored in Config's timeline and simultaneously emits a compliance change event to EventBridge.

The restricted-ssh managed rule is a periodic and change-triggered rule that evaluates whether any inbound rule on a security group allows unrestricted SSH access (port 22, protocol TCP, source 0.0.0.0/0 or ::/0). It runs on EC2 security group resources and requires no custom Lambda function — AWS owns the evaluation logic.

graph LR A["Engineer changes SG inbound rule"] --> B["AWS Config Recorder captures config item"] B --> C["restricted-ssh rule evaluates SG"] C --> D{"Port 22 open to 0.0.0.0/0?"} D -- Yes --> E["Mark NON_COMPLIANT"] D -- No --> F["Mark COMPLIANT"] E --> G["Compliance change event emitted to EventBridge"] G --> H["EventBridge rule matches event pattern"] H --> I["SNS Topic publishes message"] I --> J["Email / PagerDuty / Slack alert"]
  1. Security group change — an engineer modifies an inbound rule via console, CLI, or IaC.
  2. Config recorder captures the new configuration item and triggers rule evaluation.
  3. restricted-ssh rule evaluates the inbound rules; marks the SG NON_COMPLIANT if port 22 is open to 0.0.0.0/0 or ::/0.
  4. Compliance change event is emitted to EventBridge (event source: aws.config).
  5. EventBridge rule matches the event and routes it to the SNS topic.
  6. SNS delivers the notification to your subscribed endpoint (email, Slack via Lambda, PagerDuty, etc.).

Prerequisites

  • AWS Config enabled in the target region with an EC2 configuration recorder active.
  • An S3 bucket and optionally an SNS delivery channel configured for Config (required for Config setup, separate from the alert SNS topic).
  • IAM permissions to create Config rules, SNS topics, and EventBridge rules.

If Config isn't recording EC2 resources yet, the managed rule will deploy but never evaluate anything. That's the silent failure mode most teams hit first.

Step 1: Verify AWS Config Is Recording Security Groups

Before deploying the rule, confirm the Config recorder is active and includes EC2 resources. A rule with no recorder is a rule that never fires — and you won't get an error, just silence.

aws configservice describe-configuration-recorders \
  --region us-east-1

Check that the output shows "recordingGroup" with either "allSupported": true or EC2:SecurityGroup explicitly included, and that the recorder status is "recording": true:

aws configservice describe-configuration-recorder-status \
  --region us-east-1

If Config isn't enabled, you'll need to set up a delivery channel and recorder. The following creates a recorder that captures all supported resource types:

🔽 Click to expand: Enable AWS Config recorder (if not already active)
# Create the Config service-linked role if it doesn't exist
aws iam create-service-linked-role \
  --aws-service-name config.amazonaws.com

# Create a delivery channel (requires an existing S3 bucket)
aws configservice put-delivery-channel \
  --delivery-channel '{
    "name": "default",
    "s3BucketName": "my-config-bucket-123456789012",
    "configSnapshotDeliveryProperties": {
      "deliveryFrequency": "TwentyFour_Hours"
    }
  }' \
  --region us-east-1

# Create and start the configuration recorder
aws configservice put-configuration-recorder \
  --configuration-recorder '{
    "name": "default",
    "roleARN": "arn:aws:iam::123456789012:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig",
    "recordingGroup": {
      "allSupported": true,
      "includeGlobalResourceTypes": false
    }
  }' \
  --region us-east-1

aws configservice start-configuration-recorder \
  --configuration-recorder-name default \
  --region us-east-1

Step 2: Deploy the restricted-ssh AWS Config Managed Rule

The restricted-ssh rule is an AWS-managed Config rule. You don't write evaluation logic — you just activate it. It evaluates AWS::EC2::SecurityGroup resources and marks any security group NON_COMPLIANT if it contains an inbound rule permitting TCP port 22 from 0.0.0.0/0 or ::/0.

aws configservice put-config-rule \
  --config-rule '{
    "ConfigRuleName": "restricted-ssh",
    "Source": {
      "Owner": "AWS",
      "SourceIdentifier": "INCOMING_SSH_DISABLED"
    }
  }' \
  --region us-east-1

The SourceIdentifier for this rule is INCOMING_SSH_DISABLED — that's the documented identifier in the AWS Config Managed Rules reference. After deployment, Config will run an initial evaluation against all existing security groups in the region.

Verify the rule is active:

aws configservice describe-config-rules \
  --config-rule-names restricted-ssh \
  --region us-east-1

Look for "ConfigRuleState": "ACTIVE" in the response. If it shows EVALUATING, wait a minute and re-run — the initial evaluation sweep takes time proportional to how many security groups exist in the account.

Step 3: Create the SNS Topic for Compliance Alerts

This SNS topic is the alert destination. It's separate from any SNS topic used by Config's delivery channel. Create it, then subscribe your notification endpoint.

# Create the topic
aws sns create-topic \
  --name config-ssh-compliance-alerts \
  --region us-east-1

The command returns the topic ARN — note it, you'll need it in the next two steps. It will look like arn:aws:sns:us-east-1:123456789012:config-ssh-compliance-alerts.

# Subscribe an email endpoint
aws sns subscribe \
  --topic-arn arn:aws:sns:us-east-1:123456789012:config-ssh-compliance-alerts \
  --protocol email \
  --notification-endpoint security-team@example.com \
  --region us-east-1

The email address will receive a confirmation request. The subscription stays in PendingConfirmation state until confirmed — EventBridge can still publish to the topic, but unconfirmed email subscriptions won't receive messages.

Step 4: Create the SNS Topic Policy

EventBridge needs permission to publish to your SNS topic. Without this policy, the EventBridge rule will silently fail to deliver — no error in EventBridge, no message in SNS, no alert. This is the most common wiring mistake.

aws sns set-topic-attributes \
  --topic-arn arn:aws:sns:us-east-1:123456789012:config-ssh-compliance-alerts \
  --attribute-name Policy \
  --attribute-value '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "AllowEventBridgePublish",
        "Effect": "Allow",
        "Principal": {
          "Service": "events.amazonaws.com"
        },
        "Action": "sns:Publish",
        "Resource": "arn:aws:sns:us-east-1:123456789012:config-ssh-compliance-alerts",
        "Condition": {
          "ArnLike": {
            "aws:SourceArn": "arn:aws:events:us-east-1:123456789012:rule/*"
          }
        }
      }
    ]
  }' \
  --region us-east-1

Step 5: Create the EventBridge Rule to Route Config Compliance Events

AWS Config emits a compliance change event to EventBridge whenever a resource's compliance status changes. The event source is aws.config and the detail type is Config Rules Compliance Change. You filter on the specific rule name and the NON_COMPLIANT status so you're not flooded with COMPLIANT notifications every time someone fixes a rule.

# Create the EventBridge rule with an event pattern filter
aws events put-rule \
  --name config-ssh-noncompliant-alert \
  --event-pattern '{
    "source": ["aws.config"],
    "detail-type": ["Config Rules Compliance Change"],
    "detail": {
      "configRuleName": ["restricted-ssh"],
      "newEvaluationResult": {
        "complianceType": ["NON_COMPLIANT"]
      }
    }
  }' \
  --state ENABLED \
  --region us-east-1
# Add the SNS topic as the target
aws events put-targets \
  --rule config-ssh-noncompliant-alert \
  --targets '[
    {
      "Id": "sns-compliance-alert",
      "Arn": "arn:aws:sns:us-east-1:123456789012:config-ssh-compliance-alerts"
    }
  ]' \
  --region us-east-1

The event pattern uses newEvaluationResult.complianceType to match only transitions into NON_COMPLIANT. If you omit this filter, you'll also receive events when the security group is remediated and returns to COMPLIANT — which is noisy but sometimes useful for audit trails.

graph TD A["EventBridge Rule config-ssh-noncompliant-alert"] --> B["Event Pattern Filter source: aws.config detail-type: Config Rules Compliance Change configRuleName: restricted-ssh complianceType: NON_COMPLIANT"] B --> C["Target: SNS Topic ARN arn:aws:sns:us-east-1:...:config-ssh-compliance-alerts"] C --> D["SNS Topic Policy Allows events.amazonaws.com to call sns:Publish"] D --> E["Subscribed Endpoints Email / Lambda / HTTP"]
  1. Event pattern matches only aws.config events from the restricted-ssh rule with NON_COMPLIANT status.
  2. Target is the SNS topic ARN — EventBridge publishes the raw Config event JSON as the message body.
  3. SNS topic policy permits events.amazonaws.com to call sns:Publish — without this, delivery fails silently.

Step 6: Verify End-to-End With a Test Security Group

Don't trust the setup until you've seen it fire. Create a throwaway security group, add the offending rule, and watch the pipeline respond. This also tells you the actual detection latency in your environment — Config change-triggered rules typically evaluate within a few minutes of the resource change, but this depends on Config's internal scheduling.

# Create a test security group in the default VPC
aws ec2 create-security-group \
  --group-name test-ssh-open \
  --description 'Temporary test for Config rule verification' \
  --region us-east-1

Note the returned GroupId (e.g., sg-0abc123def456), then add the non-compliant rule:

aws ec2 authorize-security-group-ingress \
  --group-id sg-0abc123def456 \
  --protocol tcp \
  --port 22 \
  --cidr 0.0.0.0/0 \
  --region us-east-1

After a few minutes, check the compliance status directly:

aws configservice get-compliance-details-by-resource \
  --resource-type AWS::EC2::SecurityGroup \
  --resource-id sg-0abc123def456 \
  --region us-east-1

You should see "ComplianceType": "NON_COMPLIANT" in the results. If the email subscription was confirmed, you should also have received an SNS notification by this point.

Clean up after verification:

aws ec2 revoke-security-group-ingress \
  --group-id sg-0abc123def456 \
  --protocol tcp \
  --port 22 \
  --cidr 0.0.0.0/0 \
  --region us-east-1

aws ec2 delete-security-group \
  --group-id sg-0abc123def456 \
  --region us-east-1

The Misdiagnosis That Wastes an Hour

Here's a pattern that comes up repeatedly: you deploy the rule, add the test inbound rule, wait ten minutes, and nothing happens. No compliance change in Config, no SNS message. The instinct is to blame EventBridge routing or the SNS topic policy.

The actual cause is almost always that the Config recorder isn't recording AWS::EC2::SecurityGroup resources — either because Config was set up with a custom resource type list that excluded EC2, or because the recorder was stopped at some point and never restarted. Config evaluates resources based on configuration items it has recorded. If it never recorded the security group change, the rule never ran.

The fix is to re-check recorder status and resource coverage, not to redeploy the EventBridge rule:

aws configservice describe-configuration-recorder-status \
  --region us-east-1

aws configservice describe-configuration-recorders \
  --region us-east-1

If "recording": false appears in the status output, restart the recorder and trigger a manual rule evaluation to catch up:

aws configservice start-configuration-recorder \
  --configuration-recorder-name default \
  --region us-east-1

aws configservice start-config-rules-evaluation \
  --config-rule-names restricted-ssh \
  --region us-east-1
The Config recorder is like a security camera. The managed rule is the motion-detection algorithm. If the camera is off, the algorithm never sees anything — and it won't tell you the camera is off.

IAM Permissions Required

The principal deploying this setup needs the following permissions. Read/List actions on Config and EC2 require "Resource": "*" as these actions don't support resource-level restrictions in IAM.

🔽 Click to expand: IAM policy for deployment principal
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ConfigRuleManagement",
      "Effect": "Allow",
      "Action": [
        "config:PutConfigRule",
        "config:DescribeConfigRules",
        "config:DescribeConfigurationRecorders",
        "config:DescribeConfigurationRecorderStatus",
        "config:StartConfigRulesEvaluation",
        "config:GetComplianceDetailsByResource",
        "config:PutDeliveryChannel",
        "config:PutConfigurationRecorder",
        "config:StartConfigurationRecorder"
      ],
      "Resource": "*"
    },
    {
      "Sid": "SNSManagement",
      "Effect": "Allow",
      "Action": [
        "sns:CreateTopic",
        "sns:Subscribe",
        "sns:SetTopicAttributes"
      ],
      "Resource": "arn:aws:sns:us-east-1:123456789012:config-ssh-compliance-alerts"
    },
    {
      "Sid": "EventBridgeManagement",
      "Effect": "Allow",
      "Action": [
        "events:PutRule",
        "events:PutTargets",
        "events:DescribeRule"
      ],
      "Resource": "arn:aws:events:us-east-1:123456789012:rule/config-ssh-noncompliant-alert"
    },
    {
      "Sid": "EC2TestSGManagement",
      "Effect": "Allow",
      "Action": [
        "ec2:CreateSecurityGroup",
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:RevokeSecurityGroupIngress",
        "ec2:DeleteSecurityGroup",
        "ec2:DescribeSecurityGroups"
      ],
      "Resource": "*"
    },
    {
      "Sid": "IAMServiceLinkedRole",
      "Effect": "Allow",
      "Action": "iam:CreateServiceLinkedRole",
      "Resource": "arn:aws:iam::123456789012:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig"
    }
  ]
}

Extending This Pattern: Beyond Port 22

The restricted-ssh rule covers port 22 specifically. For port 3389 (RDP), AWS provides a separate managed rule with the SourceIdentifier RESTRICTED_INCOMING_TRAFFIC, which accepts parameters to specify which ports to restrict. Check the AWS Config Managed Rules documentation for the current parameter schema before deploying — parameter names and valid values are documented per rule.

For custom port combinations or more complex logic (e.g., flagging any inbound rule that opens a port range wider than a defined threshold), you'd need a custom Config rule backed by a Lambda function. The EventBridge-to-SNS wiring in steps 3–5 remains identical regardless of whether the rule is managed or custom.

One non-obvious interaction worth knowing: if you're using AWS Organizations with delegated Config administration, managed rules deployed from the management account or delegated administrator propagate to member accounts as conformance pack rules. The compliance events still flow through EventBridge in each member account, not centrally — so the SNS alert wiring needs to exist in each account, or you need an EventBridge cross-account event bus setup to centralize them.

Wrap-Up: Detecting Non-Compliant Security Group Rules With AWS Config

The full pipeline — Config recorder → restricted-ssh managed rule → EventBridge compliance change event → SNS alert — gives you automated, near-real-time detection of port 22 exposure without writing any custom evaluation code. The recorder being active and covering EC2 resources is the single most important prerequisite; everything downstream depends on it.

For production use, consider pairing this with AWS Config's auto-remediation feature, which can trigger an SSM Automation document to automatically revoke the offending rule. That's a separate setup, but the compliance detection pipeline you've built here is the prerequisite for it.

Glossary

TermDefinition
Configuration ItemA point-in-time snapshot of a supported AWS resource's configuration, recorded by the AWS Config recorder.
Managed RuleA pre-built AWS Config evaluation rule maintained by AWS, identified by a SourceIdentifier string. No Lambda function required.
Compliance Change EventAn EventBridge event emitted by AWS Config when a resource's evaluation result transitions between COMPLIANT, NON_COMPLIANT, or NOT_APPLICABLE.
Configuration RecorderThe AWS Config component that detects and records resource configuration changes. Must be active and scoped to include the resource types you want evaluated.
restricted-sshThe AWS Config managed rule (SourceIdentifier: INCOMING_SSH_DISABLED) that flags security groups permitting unrestricted inbound TCP port 22 access.

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Fix Internet Gateway and Route Table

EC2 SSH Connection Timeout: Which Security Group Rules to Check

Difference Between IAM User and IAM Role: Which One Should Your EC2 Use?