[PYTHON] Automatically delete AWS SG full open rule [Event Bridge, Lambda]

Introduction

I think that AWS SG rule automatic deletion is nothing but full open of ssh and RDP if it is AWS managed. I just wanted to touch EventBridge and Lambda, so I implemented it while investigating various things.

Constitution

Create SG → Detect with Event Bridge → Automatically delete with Lambda

Requirement definition

  1. The target to be deleted is the SG inbound rule, and the permitted source IP address is 0.0.0.0/0, ::/0.
  2. For newly created rules
  3. If a rule whose source IP address is fully open and a rule that is not are created together, delete only the fully open rule.

Creating EventBridge rules

EventBridge detects the received event that matches the rule, calls the AWS resource registered as the target, and passes the event. The called AWS resource uses the passed event to perform the specified processing. This time, we will create two EventBridge rules. I considered whether it could be implemented with one rule somehow, but I think it cannot be implemented with the filtering that can be used with the current event pattern. If you can implement it, please let me know in the comments.

Event pattern

Since we will create an event pattern to detect the full open SG, we will check the format of the event to be detected.

AWS official documentation Example EventBridge event from a supported AWS service https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/event-types.html

If you check the official document above, you can't find any SG events, so the events delivered via CloudTrail are applicable. The format was as follows.

Event format example


{
    "version": "0",
    "id": "36eb8523-97d0-4518-b33d-ee3579ff19f0",
    "detail-type": "AWS API Call via CloudTrail",
    "source": "aws.s3",
    "account": "123456789012",
    "time": "2016-02-20T01:09:13Z",
    "region": "us-east-1",
    "resources": [],
    "detail": {
        "eventVersion": "1.03",
        "userIdentity": {
            "type": "Root",
            "principalId": "123456789012",
            "arn": "arn:aws:iam::123456789012:root",
            "accountId": "123456789012",
            "sessionContext": {
                "attributes": {
                    "mfaAuthenticated": "false",
                    "creationDate": "2016-02-20T01:05:59Z"
                }
            }
        },
        "eventTime": "2016-02-20T01:09:13Z",
        "eventSource": "s3.amazonaws.com",
        "eventName": "CreateBucket",
        "awsRegion": "us-east-1",
        "sourceIPAddress": "100.100.100.100",
        "userAgent": "[S3Console/0.4]",
        "requestParameters": {
            "bucketName": "bucket-test-iad"
        },
        "responseElements": null,
        "requestID": "9D767BCC3B4E7487",
        "eventID": "24ba271e-d595-4e66-a7fd-9c16cbf8abae",
        "eventType": "AwsApiCall"
    }
}

In the example, it is S3, so replace it with SG rule creation. I think that the contents of detail will be replaced with the CloudTrail log as it is, so actually create a full open SG, check the CloudTrail log, and replace it with the contents (I put a star mark on the property used in the event pattern later. .). Now you know what event you want to detect.

Event format example (SG rule creation version)


{
    "version": "0",
    "id": "36eb8523-97d0-4518-b33d-ee3579ff19f0",
   ★"detail-type": "AWS API Call via CloudTrail",
   ★"source": "aws.ec2",
    "account": "123456789012",
    "time": "2016-02-20T01:09:13Z",
    "region": "us-east-1",
    "resources": [],
    "detail": {
        "eventVersion": "1.08",
        "userIdentity": {
Abbreviation
        },
        "eventTime": "2021-01-09T04:08:28Z",
   ★    "eventSource": "ec2.amazonaws.com",
   ★    "eventName": "AuthorizeSecurityGroupIngress",
        "awsRegion": "us-east-1",
        "sourceIPAddress": "192.168.1.1",
        "userAgent": "console.ec2.amazonaws.com",
        "requestParameters": {
            "groupId": "sg-000000000000",
            "ipPermissions": {
                 "items": [
                    {
                        "ipProtocol": "-1",
                        "groups": {},
                        "ipRanges": {
                            "items": [
                                {
   ★                                "cidrIp": "0.0.0.0/0"
                                }
                            ]
                        },
                        "ipv6Ranges": {
                            "items": [
                                 {
   ★                                "cidrIpv6": "::/0"
                                }
                            ]
                        },
                        "prefixListIds": {}
                    }
                ]
            }
         },
The following is omitted

From now on, we will create an event pattern. This event does not correspond to the event pattern defined in AWS, so I will create it myself. There are three events that must be detected, ・ IPv4 only fully open ・ IPv6 only fully open ・ Fully open for both IPv4 and IPv6 SG rule. I will create two rules, so start from the first. Here is the completed event pattern.

Event pattern ①


{
    "source": ["aws.ec2"],
    "detail-type": ["AWS API Call via CloudTrail"],
    "detail": {
        "eventSource": ["ec2.amazonaws.com"],
        "eventName": ["AuthorizeSecurityGroupIngress"],
        "requestParameters": {
            "ipPermissions": {
                "items": {
                    "ipRanges": {
                        "items": {
                            "cidrIp": [ { "cidr": "0.0.0.0/0" } ]
                        }
                    }
                }
            }
        }
    }
}

What this event pattern detects ・ IPv4 only fully open ・ Fully open for both IPv4 and IPv6 SG rule that is fully open only for IPv6 cannot be detected. Since only IPv6 only detects fully open SG rules, another EventBridge rule has the following event pattern.

Event pattern ②


{
    "source": ["aws.ec2"],
    "detail-type": ["AWS API Call via CloudTrail"],
    "detail": {
        "eventSource": ["ec2.amazonaws.com"],
        "eventName": ["AuthorizeSecurityGroupIngress"],
        "requestParameters": {
            "ipPermissions": {
                "items": {
                    "ipRanges": {
                        "items": {
                            "cidrIp": [ { "anything-but" : "0.0.0.0/0" }, { "exists": false } ]
                        }
                    },
                    "ipv6Ranges": {
                        "items": {
                            "cidrIpv6": [ "::/0" ]
                        }
                    }
                }
            }
        }
    }
}

It is a little more complicated than event pattern (1) so that two EventBridge rules do not detect one event twice. If the ipRanges property is not described in event pattern ②, the SG rule creation event of IPv4 / IPv6 fully open will match in both event patterns ① and ②, and will be detected by the two EventBridg e rules. Will be done. As a result, it will be processed by the same Lambda (Lambda is not divided into two), and one of them will always result in an error. To prevent it -IPv6 fully open and IPv4 is specified or IPv4 is not permitted The one that matches the event is created as event pattern ②. As a result, the EventBridge rule will detect the event only for the SG rule creation event that is fully open for IPv6 only.

The table below summarizes the correspondence.

pattern detection
Event pattern ① IPv4 only fully open
Both IPv4 and IPv6 are fully open
Event pattern ② IPv6 only fully open

We also use content filtering for each event pattern. If you want to know more details, please see the following documents. In addition, in the {" anything-but ":" 0.0.0.0/0 "} of the event pattern ②, the IP address matching is not used like the event pattern ① in the anything-but. The use of IP address matching was not supported.

AWS official documentation Content-based filtering using event patterns https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/content-filtering-with-event-patterns.html

Event to pass to target

EventBridge can customize the content of the event to be passed to the target, or create and pass JSON unrelated to the event instead of the event. This time, I will pass only the necessary parts in Lambda. Select Input Settings> Part of Matched Events on the Management Console and enter $ .detail.requestParameters. When $ .detail.requestParameters is set to JSON, it is as follows. Use the required information such as group ID from this JSON.

JSON:$.detail.requestParameters


{
    "groupId": "sg-000000000000",
    "ipPermissions": {
        "items": [
            {
                "ipProtocol": "-1",
                "groups": {},
                "ipRanges": {
                    "items": [
                        {
                               "cidrIp": "0.0.0.0/0"
                        }
                    ]
                },
                "ipv6Ranges": {
                    "items": [
                        {
                               "cidrIpv6": "::/0"
                        }
                    ]
                },
                "prefixListIds": {}
            }
        ]
    }
}

Created by Lambda

Created with Python3.6. I'm using the revoke_security_group_ingress () method to delete an SG rule. Here is the completed program. As mentioned in the comments, when deleting a rule that does not set the port number with ec2.revoke_security_group_ingress (), put an appropriate number in fromPort and toPort and ec2.revoke_security_group_ingress () Even if I executed , it did not cause an error and could be deleted. At first, I tried putting None in each port to see if it could be supported, but I was angry that it was useless unless it was an int type.

Lambda functions


import json
import boto3

ec2 = boto3.client("ec2")

def lambda_handler(event, context):

    ip_permissions_items = event["ipPermissions"]["items"]
    SGID = event["groupId"]

    for ip_permissions_item in ip_permissions_items:
        ip_protocol = ip_permissions_item["ipProtocol"]

        ip_ranges = ip_permissions_item["ipRanges"]
        ipv6_ranges = ip_permissions_item["ipv6Ranges"]
        
        #If cidrip of ipRanges exists and is fully open, store it in a variable as it is, otherwise store None
        if ip_ranges == {}:
            cidrip = None
        else:
            #Since there may be multiple IP addresses to allow, check the IP addresses one by one, and when the full open occurs, store it in a variable and exit.
            for ip_ranges_item in ip_ranges["items"]:
                if ip_ranges_item["cidrIp"] == "0.0.0.0/0":
                    cidrip = ip_ranges_item["cidrIp"]
                    break
                else:
                    cidrip = None

        if ipv6_ranges == {}:
            cidripv6 = None
        else:
            for ipv6_ranges_item in ipv6_ranges["items"]:
                if ipv6_ranges_item["cidrIpv6"] == "::/0":
                    cidripv6 = ipv6_ranges_item["cidrIpv6"]
                    break
                else:
                    cidripv6 = None

        #Since the port setting does not exist depending on the rule, store it in the variable as it is if it exists, and store an appropriate number if it does not exist.
        if "fromPort" and "toPort" in ip_permissions_item:
            from_port = ip_permissions_item["fromPort"]
            to_port = ip_permissions_item["toPort"]
        else:
            #If it is not an int type, an error will occur when deleting, so a numerical value will be stored.
            #Since it is a process when the port does not exist, it seems that there is no problem with any numerical value(0,70000 is no problem)
            from_port = 0
            to_port = 0

        if cidrip == "0.0.0.0/0" and cidripv6 == "::/0":
            ec2.revoke_security_group_ingress(
                GroupId=SGID,
                IpPermissions=[
                    {
                        "IpProtocol": ip_protocol,
                        "FromPort": from_port,
                        "ToPort": to_port,
                        "IpRanges":[
                            {
                                "CidrIp": cidrip
                            }
                        ],
                        "Ipv6Ranges":[
                            {
                                "CidrIpv6": cidripv6
                            }
                        ]
                    }
                ]
            )
        elif cidrip == "0.0.0.0/0":
                ec2.revoke_security_group_ingress(
                    GroupId=SGID,
                    IpPermissions=[
                        {
                            "IpProtocol": ip_protocol,
                            "FromPort": from_port,
                            "ToPort": to_port,
                            "IpRanges":[
                                {
                                    "CidrIp": cidrip
                                }
                            ]
                        }
                    ]
                )
        elif cidripv6 == "::/0":
                ec2.revoke_security_group_ingress(
                    GroupId=SGID,
                    IpPermissions=[
                        {
                            "IpProtocol": ip_protocol,
                            "FromPort": from_port,
                            "ToPort": to_port,
                            "Ipv6Ranges":[
                                {
                                    "CidrIpv6": cidripv6
                                }
                            ]
                        }
                    ]
                )

This completes the automatic deletion implementation of the SG full open rule. It seemed to work fine as far as I tried, but if there are any flaws I would appreciate it if you could let me know in the comments. I wrote the article mainly for my personal study, but I hope it helps someone.

Recommended Posts

Automatically delete AWS SG full open rule [Event Bridge, Lambda]