LoginSignup
0
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-01-09

はじめに

 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 経由で配信されたイベントが該当します。
 書式は以下のとおりでした。

イベントの書式例
{
    "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ログを確認し、その内容に置き換えます(後ほどイベントパターンで使うプロパティに星マークを付けています。)。
 これで、検出対象のイベントが分かりました。

イベントの書式例(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ルールです。
 ルールを二つ作成するので、まず一つ目から。
 完成したイベントパターンがこちらです。

イベントパターン①
{
    "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ルールは以下のイベントパターンにします。

イベントパターン②
{
    "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ルール作成イベントが、イベントパターン①、②の両方で一致してしまい、二つのEventBridgeルールに検出されてしまいます。
 その結果、同じLambdaで処理されることになり(Lambdaは二つに分けません。)、どちらかが必ずエラーとなります。
 それを防ぐために、
  ・IPv6フルオープン かつ IPv4が指定されている または IPv4を許可していない
イベントに一致するものをイベントパターン②として作成しています。
 これにより、IPv6のみフルオープンのSGルール作成イベントに限り、EventBridgeルールがイベントを検出します。

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

パターン 検出
イベントパターン① IPv4のみフルオープン
IPv4・IPv6両方フルオープン
イベントパターン② 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などを必要な情報を使用します。

$.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()でポート番号の設定をしていないルールを削除する際、適当な数字をfromPorttoPortに入れてec2.revoke_security_group_ingress()を実行してもエラーにならず削除できました。
 最初は各ポートにNoneを入れて対応できないか試しましたが、int型でなければダメだと怒られました。

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1