AWSのSGフルオープンルールを自動削除する【EventBridge, Lambda】

# はじめに
　AWSのSGルール自動削除は、AWSマネージドだとsshやRDPのフルオープン以外はないと思います。
　ちょうどEventBridgeやLambdaを触ってみたかったので、いろいろ調べつつ実装してみました。

# 構成

　SG作成 → EventBridgeで検出 → Lambdaで自動削除

# 要件定義

1. 削除対象はSGのインバウンドルールで、許可されている送信元IPアドレスが`0.0.0.0/0`,`::/0`となっているもの
2. 新規作成されたルールが対象
3. 送信元IPアドレスがフルオープンになっているルールと、そうでないルールが一緒に作成された場合、フルオープンのルールのみを削除する

# EventBridgeルールの作成

　EventBridgeは、ルールにより一致した受信イベントを検出し、ターゲットとして登録したAWSリソースを呼び出してイベントを渡します。
　呼び出されたAWSリソースは渡されたイベントを使用したりして、決められた処理を実行します。
　今回、EventBridgeルールは二つ作成します。
　どうにかして一つのルールで実装できないか検討しましたが、現状のイベントパターンで使用できるフィルタリングでは実装できないと思います。
　もし実装できたらコメント欄で教えていただけると幸いです。

## イベントパターン

　フルオープンSGを検出するためのイベントパターンを作成するので、検出対象のイベントがどのような形式なのかを確認します。

>AWS公式ドキュメント
>サポートされている AWS サービスからの EventBridge イベントの例
>https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/event-types.html

　上記公式ドキュメントを確認すると、SGのイベントは見当たらないので、CloudTrail 経由で配信されたイベントが該当します。
　書式は以下のとおりでした。

```JSON:イベントの書式例
{
    "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"
    }
}
```

　例ではS3のため、これをSGルール作成に置き換えます。
　`detail`の中身はCloudTrailログでそのまま置き換えられる思いますので、実際にフルオープンSGを作成してCloudTrailログを確認し、その内容に置き換えます（後ほどイベントパターンで使うプロパティに星マークを付けています。）。
　これで、検出対象のイベントが分かりました。

```JSON:イベントの書式例（SGルール作成版）
{
    "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": {
            略
        },
        "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": {}
                    }
                ]
            }
         },
        以下略

```

　これからは、イベントパターンを作成します。
　今回のイベントはAWSで定義したイベントパターンには該当しないので、自作します。
　検出しなければならないイベントは、三つあり、
　　・IPv4のみフルオープン
　　・IPv6のみフルオープン
　　・IPv4・IPv6両方フルオープン
のSGルールです。
　ルールを二つ作成するので、まず一つ目から。
　完成したイベントパターンがこちらです。

```JSON:イベントパターン①
{
    "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" } ]
                        }
                    }
                }
            }
        }
    }
}
```

　このイベントパターンが検出する対象は
　　・IPv4のみフルオープン
　　・IPv4・IPv6両方フルオープン
のSGルールであり、IPv6のみフルオープンのSGルールは検出できません。
　IPv6のみフルオープンのSGルールだけを検出するため、もう一つのEventBridgeルールは以下のイベントパターンにします。

```JSON:イベントパターン②
{
    "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" ]
                        }
                    }
                }
            }
        }
    }
}
```

　イベントパターン①より少し複雑になっているのは、二つのEventBridgeルールが一つのイベントを二重に検出しないようにするためです。
　イベントパターン②に`ipRanges`プロパティの記載がない場合、`IPv4・IPv6両方フルオープン`のSGルール作成イベントが、イベントパターン①、②の両方で一致してしまい、二つのEventBridgeルールに検出されてしまいます。
　その結果、同じLambdaで処理されることになり（Lambdaは二つに分けません。）、どちらかが必ずエラーとなります。
　それを防ぐために、
　　・IPv6フルオープン　かつ　IPv4が指定されている　または　IPv4を許可していない
イベントに一致するものをイベントパターン②として作成しています。
　これにより、IPv6のみフルオープンのSGルール作成イベントに限り、EventBridgeルールがイベントを検出します。

対応関係をまとめると以下の表のとおりです。

|パターン|検出|
|---|---|
|イベントパターン①|IPv4のみフルオープン<br>IPv4・IPv６両方フルオープン|
|イベントパターン②|IPv6のみフルオープン|

　また、それぞれのイベントパターンでコンテンツフィルタリングを使用しています。
　詳細を知りたい方は以下のドキュメントをご覧ください。
　なお、イベントパターン②の`{ "anything-but" : "0.0.0.0/0" }`で、 イベントパターン①のようにIPアドレスマッチングを使用していないのは、`anything-but`内でのIPアドレスマッチングの使用がサポートされていなかったからです。

>AWS公式ドキュメント
>イベントパターンを使用したコンテンツベースのフィルタリング
>https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/content-filtering-with-event-patterns.html

## ターゲットに渡すイベント

　EventBridgeはターゲットに渡すイベント内容をカスタマイズしたり、イベントの代わりにイベントと関係のないJSONを自作して渡すことができます。
　今回は、Lambdaで必要な部分のみ渡すことにします。
　マネージメントコンソール上の「入力の設定」→ 「一致したイベントの一部」を選択し、`$.detail.requestParameters`を入力します。
　`$.detail.requestParameters`をJSONにすると以下のとおりです。
　このJSONからグループIDなどを必要な情報を使用します。

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

# Lambda作成

　`Python3.6`で作成しました。
　SGルールを削除する`revoke_security_group_ingress()`メソッドを使用しています。
　完成したプログラムがこちらです。
　コメントでも記載していますが、`ec2.revoke_security_group_ingress()`でポート番号の設定をしていないルールを削除する際、適当な数字を`fromPort`と`toPort`に入れて`ec2.revoke_security_group_ingress()`を実行してもエラーにならず削除できました。
　最初は各ポートに`None`を入れて対応できないか試しましたが、int型でなければダメだと怒られました。

```python:Lambdaの関数
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"]
        
        # ipRangesのcidripが存在しフルオープンであればそのまま変数に格納し、それ以外はNoneを格納する
        if ip_ranges == {}:
            cidrip = None
        else:
            # 許可するIPアドレスが複数存在する場合もあるので順次IPアドレスを調べ、フルオープンがでた時点で変数に格納し抜け出す
            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

        # ポート設定はルールによっては存在しないため、あればそのまま変数に格納し、なければ適当な数字を格納する
        if "fromPort" and "toPort" in ip_permissions_item:
            from_port = ip_permissions_item["fromPort"]
            to_port = ip_permissions_item["toPort"]
        else:
            # int型でなければ削除時エラーが発生するため数値を格納
            # ポートが存在しない場合の処理なので、どの数値でも問題ないと思われる(0,70000は支障なし)
            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
                                }
                            ]
                        }
                    ]
                )
```

　これでSGフルオープンルールの自動削除実装は終わりです。
　私が試してみた限りはうまく動いているように見えましたが、欠陥があった場合はコメント欄から教えていただけると嬉しいです。
　主に私個人の勉強のために記事を執筆しましたが、誰かのお役に立てれば幸いです。