はじめに

唐突ですが、セキュリティグループを削除するときにエラーになった経験はありますか?

セキュリティグループ削除時エラーの原因として、自身や他のセキュリティグループのルールで参照されている場合やEC2やRDSなどのリソースに紐づいていること等が挙げられます。

特定セキュリティグループを削除する際に参照している許可ルールを一覧化して、セキュリティグループの削除に問題ないか確認したいといった要望があります。
特定のセキュリティグループの参照先が1つ2つと少ないのであれば確認は容易でありますが、検証環境などで九龍城のように複雑に重なりあった違法建築ルールの場合1つずつ紐解いていくことは精神的にも時間的にも避けたい作業になります。

そこで、特定のセキュリティグループを参照しているルールだけをサクサク一覧化しよう、というのが本記事の議題となります。

AWS提供のセキュリティグループ関連付けの確認方法

本題に入る前にまず、AWSから提供されている特定のセキュリティグループを参照しているセキュリティグループルールの確認方法です。

AWSコンソールでの確認

まず、コンソールの画面での確認方法です。
[EC2] – [セキュリティグループ] の画面から、検索フィルタリングの項目で「ソース/送信先 (グループ ID)」で参照しているルールを一覧化することができます。
以下の例では、末尾sg-xxxxxxxxxxxxxx2e9のセキュリティグループで検索しています。
検索後に表示されたセキュリティグループ1つ1つを選択し、インバウンド・アウトバウンドそれぞれでsg-xxxxxxxxxxxxxx2e9の関連付けがないか確認する必要があります。

AWS CLIでの確認

次に、AWS CLIを用いた確認方法です。

次のdescribe-security-groupsコマンドを実行することで確認することができます。
インバウンドではなくアウトバウンドの確認をしたい場合、filtersオプションのip-permission.group-idegress.ip-permission.group-idに置換してコマンドを実行する必要があります。

$ aws ec2 describe-security-groups --filters Name=ip-permission.group-id,Values=sg-xxxxxxxxxxxxxx2e9

コマンドを実行すると以下の様に、確認したい特定のセキュリティグループsg-xxxxxxxxxxxxxx2e9を参照しているルールを出力してくれますが、参照しているルールだけでなく他の許可ルールも併せて出力されます。

$ aws ec2 describe-security-groups --filters "Name=ip-permission.group-id,Values=sg-xxxxxxxxxxxxxx2e9"
{
    "SecurityGroups": [
        {
            "Description": "allow ssh",
            "GroupName": "ubukata-sg-ssh",
            "IpPermissions": [
                {
                    "FromPort": 22,
                    "IpProtocol": "tcp",
                    "IpRanges": [
                        {
                            "CidrIp": "172.16.0.111/32"
                        }
                    ],
                    "Ipv6Ranges": [],
                    "PrefixListIds": [],
                    "ToPort": 22,
                    "UserIdGroupPairs": [
                        {
                            "Description": "allow sg",
                            "GroupId": "sg-xxxxxxxxxxxxxx2e9",
                            "UserId": "xxxxxxxxxxxxxxxx"
                        }
                    ]
                }
            ],
            "OwnerId": "xxxxxxxxxxxxxxxx",
            "GroupId": "sg-xxxxxxxxxxxxxxxxccd",
            "IpPermissionsEgress": [
                {
                    "IpProtocol": "-1",
                    "IpRanges": [
                        {
                            "CidrIp": "0.0.0.0/0"
                        }
                    ],
                    "Ipv6Ranges": [],
                    "PrefixListIds": [],
                    "UserIdGroupPairs": []
                }
            ],
            "VpcId": "vpc-xxxxxxxxxxxxxxxxx"
        },
        (中略)
    ]
}

関連付けされたセキュリティグループルールだけがほしい

閑話休題。

前置きが長くなってしまいましたが、コンソール画面やCLIを用いた確認では、特定のセキュリティグループを参照している許可ルール以外も出力されるため、pythonを用いて特定のセキュリティグループを参照している許可ルール一覧だけを出力するコードを作成しました。

出力要件は以下の通りになります。

  • 特定のセキュリティグループを参照しているルールのみを一覧として出力したい
  • 許可しているプロトコルやポート、インバウンド/アウトバウンドを網羅したい
  • 確認、引数として渡しやすさのためCSV形式で出力したい

コード

出力要件を満たすpythonコードは以下になります。output_referencedSg.pyなど適当な名前で保存して使用してください。
引数として特定のセキュリティグループ名またはセキュリティグループIDを指定します。

また、本コードの実行条件は以下の4つになります。

  • Python のインストール(自環境はPython 3.11.4を使用していますが、バージョンの細かい指定はありません)
  • boto3 library のインストール
  • セキュリティグループを参照するAWS権限
  • 実行環境でデフォルトリージョンが指定済みであること
import sys
import boto3
from collections import namedtuple

Rule = namedtuple(
    'Rule',
    [
        'group_name'
    ]
)


def main():
    if len(sys.argv) < 2:
        print('invalid argument. (example : python3 output_referencedSg.py SGName or SGID)')
        sys.exit(1)
    sg_name = sys.argv[1]

    client = boto3.client('ec2')

    referenced_sg_id = return_group_id(sg_name)
    found_sgs: list = []

    # inbound(ingress)
    response = client.describe_security_groups(
        Filters=[
            {
                'Name': 'ip-permission.group-id',
                'Values': [
                    referenced_sg_id
                ]
            }
        ]
    )
    sgs: list = response['SecurityGroups']
    if 'NextToekn' in response:
        print('next!')
        sgs.extend(_get_next_sgs(response['NextToken']))

    for sg in sgs:
        group_id = sg['GroupId']
        group_name = sg['GroupName']
        found_sgs.append((group_id, group_name))

    # outbound(egress)
    response = client.describe_security_groups(
        Filters=[
            {
                'Name': 'egress.ip-permission.group-id',
                'Values': [
                    referenced_sg_id
                ]
            }
        ]
    )
    sgs: list = response['SecurityGroups']
    if 'NextToekn' in response:
        print('next!')
        sgs.extend(_get_next_sgs(response['NextToken']))

    for sg in sgs:
        group_id = sg['GroupId']
        group_name = sg['GroupName']
        found_sgs.append((group_id, group_name))

    for sg in set(found_sgs):
        show_referenced_rule(sg, referenced_sg_id)


def _get_next_sgs(next_token: str):
    client = boto3.clinet('ec2')
    response = client.describe_security_group_rules(
        NextToken=next_token
    )
    sgs: list = response['SecurityGroups']
    if 'NextToken' in response:
        sgs.extend(_get_next_sgs(response['NextToken']))
    return sgs


def show_referenced_rule(sg: tuple, referenced_sg_id: str):
    group_id = sg[0]
    group_name = sg[1]
    client = boto3.client('ec2')
    response = client.describe_security_group_rules(
        Filters=[
            {
                'Name': 'group-id',
                'Values': [
                    group_id
                ]
            }
        ]
    )
    rules: list = response['SecurityGroupRules']
    if 'NextToken' in response:
        print('next!')
        rules.extend(_get_next_rules(response['NextToken']))
    for rule in rules:
        if 'ReferencedGroupInfo' in rule:
            if rule['ReferencedGroupInfo']['GroupId'] == referenced_sg_id:
                bound = 'Egress' if rule['IsEgress'] else 'Ingress'
                protocol = rule['IpProtocol']
                from_port = rule['FromPort']
                to_port = rule['ToPort']
                rule_id = rule['SecurityGroupRuleId']

                print(f'{group_name},{group_id},{bound},{protocol},{from_port},{to_port},{rule_id}')


def _get_next_rules(next_token: str):
    client = boto3.client('ec2')
    response = client.describe_security_group_rules(
        NextToken=next_token
    )
    rules: list = response['SecurityGroupRules']
    if 'NextToken' in response:
        rules.extend(_get_next_rules(response['NextToken']))
    return rules


# GroupName から GroupIDを求める、GroupID が見つからなかったら引数と判断
def return_group_id(group_name: str):
    client = boto3.client('ec2')
    response = client.describe_security_groups(
        Filters=[
            {
                'Name': 'group-name',
                'Values': [
                    group_name
                ]
            }
        ]
    )
    if response['SecurityGroups'] == []:
        return (group_name)
    else:
        return (response['SecurityGroups'][0]['GroupId'])


if __name__ == '__main__':
    main()


実行結果の例は以下のようになります。
特定のセキュリティグループを参照しているルール一覧を[セキュリティグループ名,セキュリティグループID,インバウンド/アウトバウンド,Fromポート,Toポート,参照しているルールID]の内容で出力します。

$ python3 output_referencedSg.py sg-xxxxxxxxxxxxxx2e9
ubukata-sg-http,sg-xxxxxxxxxxxxxxb07,Ingress,tcp,80,80,sgr-XXXXXXXXXXXXXXX1
ubukata-sg-http,sg-xxxxxxxxxxxxxxb07,Ingress,tcp,443,443,sgr-XXXXXXXXXXXXXXX2
ubukata-sg-rdp,sg-xxxxxxxxxxxxxxc1d,Egress,-1,-1,-1,sgr-XXXXXXXXXXXXXXX3
ubukata-sg-ssh,sg-xxxxxxxxxxxxxxccd,Ingress,tcp,22,22,sgr-XXXXXXXXXXXXXXX4

まとめ

いかがでしたでしょうか。
特定のセキュリティグループを参照している許可ルール一覧化について記載しました。
この内容がどなたかの役に立てれば幸いです。