非エンジニアでも簡単に複数の AWS アカウントのセキュリティグループ一覧を取得できるようにしてみました

セキュリティ監査の目的から、非エンジニアでも簡単に AWS CLI を利用して複数の AWS アカウントからセキュリティグループの一覧を取得できるようにした仕組みについて紹介します。
2022.07.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、札幌在住 prismatix 事業部 部門情シスチームの池田です。実は 2021年1月1日付けで AWS 事業本部 オペレーション部(通称オペ部)から異動していました。

prismatix(プリズマティクス)についてはこちらの記事で詳しく紹介されていますので是非ご覧ください。

prismatix(プリズマティクス)事業部のことがよくわかるWebページやブログエントリ、YouTube動画 n選

前置き

クラスメソッドには、全社共通基盤システム(自社ドメインでのメールアドレスや Slack Organization など)を管理する全社情シスというポジションがあります。

prismatix 事業部では、全社情共通基盤システムとは別に部門独自に採用した SaaS も多数利用しています。また、社内検証や開発用の AWS アカウント、お客様にサービスをご利用いただくための AWS アカウントも多数保有しています。

それらにおけるアカウント登録/削除などのユーザー管理や、各種セキュリティ監査を行ったりするポジションとして部門情シスチームを立ち上げました。特徴としてはチームメンバーのほとんどが IT 未経験者(非エンジニア)で構成されている点だと思っています。

今回は、前述のような背景があるこのチームにおいて、誰でも簡単に AWS CLI を利用して複数の AWS アカウントからセキュリティグループの一覧を取得できるようにした取り組みについて紹介します。

少し前の状況

私が prismatix 事業部へ異動した頃は「非エンジニアを含めて、誰でも全く同じ手順で確実に実施できる」ことを重視した仕組みで、Amazon WorkSpaces で起動した Windows 環境で Git Bash を利用するという手順が用意されていました。その手順書はスクリーンショットも多く、丁寧な内容で整備されていたのですが、毎月繰り返し行う作業としては工程が多く作業者の負担はそれなりに大きなものでした。

また、この「セキュリティグループの一覧を取得、確認をする」という作業を行う目的は「検証作業等で一時的に作成されたセキュリティグループがそのまま忘れて放置されていないこと」や「計画にはないセキュリティグループが誤操作や第三者による不正操作などで作成されていないか」をチェックすることであり、情報を取得することそのものに時間をかけすぎるのは限られた人員と時間からも改善すべきと判断しました。

具体的には、Git Bash 上で下記記事にあるワンライナーを応用したスクリプトを実行してセキュリティグループの一覧を csv 出力し、管理用 GitHub リポジトリに push して前回実施時との差分をチェックするということを行っていました。

1ライナーでAWSアカウントにあるSecurityGroupをCSVで出力してみた

もっと楽に短時間で終わらせたい

毎月繰り返し行う作業が他にもあり、それらは AWS CLI やいくつかの SaaS プロダクトの API を利用するものや Git コマンドを利用するものばかりでした。

コマンドライン操作に不慣れな非エンジニアメンバーに負担の少ない方法で実施してもらえるにはどうしたら良いか検討する中で Cloud9 を利用してみようと思い立ちました。Cloud9 であれば、その作業を行いたい時にブラウザ経由で EC2 インスタンスに配置したスクリプトを実行するといったことも簡単に行えるほか、インスタンスの停止忘れ対策として自動休止する時間を設定できる点も魅力に感じました。

やってみた

複数の AWS アカウントに対して describe-security-groups コマンドを実行し、各 AWS アカウントごとに csv ファイルへ書き出し、GitHub のリポジトリへ push する。という処理を簡単な操作で行えるようにしました。

作業手順

実際の業務で利用している手順書からざっくり抜き出すと以下のような手順になります。

  1. Cloud9 を実行できる AWS アカウントに作業用 IAM User でログインします

  2. Cloud9 コンソールへ移動し、セキュリティグループ一覧取得用の Environment を起動します

  3. 管理用 GitHub リポジトリから最新の情報を取得するためのコマンドを手順書から Cloud9 のターミナルへコピペして実行します

git clone git@github.com:PZ-GitHubName/PZ-GitHubRepoName.git

  1. Cloud9 IDE 左ペインに表示されるファイルツリーの中からメインのスクリプト main.sh を選択、右クリックメニューから「RUN」を選択します

  2. .bash_profile に function で組み込んだ関数から MFA コードの入力が求められるので入力します(参考: AWS ドキュメント

  3. 対象となる AWS アカウントのセキュリティグループ一覧を取得し終わると csv ファイルが GitHub のリポジトリへ push されますので Cloud9 を開いたブラウザは数分放置します

  4. Slack の指定チャンネルに Bot から通知が届いたら処理が完了した合図なので Cloud9 IDE タブを閉じて終了です

この手順では、作業者が入力する情報は AWS アカウントへのログイン情報のほかは git clone コマンドと MFA コードのみで、あとは通知が届くまで放置して良い(うっかり忘れても30分後には自動でインスタンスが休止します)のでかなり改善されたと思います。

新たな問題が発覚した

とても簡単に短時間で目的の情報取得ができるようになり、気持ちと時間に余裕が持てるようになったためいくつかの AWS アカウントのセキュリティグループ一覧を GitHub で眺めてみたところ、ひとつもセキュリティグループが記録されていない場合があることに気づきました。

それまでは前回との差分(GitHub の Pull request 画面の diff )をチェックするのみだったために見落としていたようです。その AWS アカウントを調べてみると、実際にはひとつだけ default セキュリティグループが存在していました。

describe-security-groups コマンドで確かめたところそのセキュリティグループは このAWS ドキュメントの「セキュリティグループの作成」のサンプルにあるように "IpPermissions": [] となっているものでした。

先に紹介した Python のワンライナーを確かめた(実際には複数のメンバーに教えてもらった)結果、"IpPermissions": [] だった場合の処理が記載されていないためにエラーとなっているようだとわかりました。

Python のワンライナーをどう変更するか、あるいはこの記事を参考に jq で if 文を使ってみるか迷いつつあれこれ試しては Slack の times に呟いていると神が降臨しました。 提案されたコードを手元の Mac 上で実行すると期待した結果が取得できることが判明したので、Cloud9 環境にあるスクリプトを入れ替えて解決と喜びました。

そうは甘くなかった

早速 Cloud9 環境で複数の AWS アカウントに対して実行できるようにと既存のスクリプトを修正してみたものの、どうやら AWS_PROFILE を Python コードに引き継げておらず UnauthorizedOperation となってしまいました。 この問題を解消するために部内メンバーに相談してみたところ、複数のメンバーからアドバイスが貰えました。結果的にはメインのスクリプトから Python コードを呼び出す際に引数として AWS_PROFILE を渡すほか、いくつかの修正を Python コードに加えることで解決できました。

修正前

import boto3
client = boto3.client("ec2")
response = client.describe_security_groups()

修正後(最終的にはもう少し手を加えています。本文最後に全行掲載しています。)

import sys
import boto3
client = boto3.session.Session(profile_name=sys.argv[1]).client("ec2")
response = client.describe_security_groups()

Python コードを呼び出す箇所

while read ID PROFILE REGION; do
  python xxx.py ${PROFILE}
done <"${ENVLIST}"

やってみた感想

普段は簡単なシェルスクリプトやワンライナー程度しかプログラムを書く機会がないため、期待通りに動作しない時の確認ポイントや考え方、Python の書き方などを多くのメンバーから教わることができました。 残念ながら全てのコードを記載することはできませんが、セキュリティグループを取得して csv ファイルに出力させるための Python コードと他のスクリプトの簡単な紹介をします。

全体の構造

  • main.sh : チェック対象 AWS アカウントのリスト ${ENVLIST} を複数ファイルに分割(GitHub Pull request 画面で差分が多い場合にブラウザ動作が重くなることがあったため複数の Pull request として回避)、後続の処理を行うためのスクリプトを順に呼び出し、全ての処理が完了したら Slack へ処理が完了したことを通知

  • bin/00_init.sh : main.sh から呼びだされ、チェック対象となる AWS アカウントを管理している kintone アプリから csv 形式で情報を取得、対象の AWS_PROFILE を .aws/config へ反映させる

  • bin/01_checkout.sh : Cloud9 環境に作業用ブランチを作成する

  • bin/02_nw_check.sh : チェック対象となる AWS アカウントごとに csv ファイルを格納するためのディレクトリを初期化、describe_security_groups.py を呼び出す

  • bin/03_push.sh : 取得した csv ファイルを管理用 GitHub リポジトリへ push する(一旦 Draft Pull request として差分の妥当性などを確認、コメント等を記載したうえで管轄チームへ Pull request で最終確認を依頼するため push までにしている)

  • bin/04_slack.sh : main.sh から呼びだされ Slack に進捗状況を通知 (他にもいくつかの設定ファイル等はありますが割愛します)

  • describe_security_groups.py : 100以上の AWS アカウントに対して describe-security-groups コマンドを実行、csv ファイルとして出力する

*なお、このコードではエラー処理を省略しています。 最後の2行で GroupName をキーにして並べ替えてから出力することで GitHub の Draft Pull request としたときに差分が見やすくなるようにしたのが個人的なチャームポイントです。

import csv
import sys
import boto3
client = boto3.session.Session(profile_name=sys.argv[1]).client("ec2")
response = client.describe_security_groups()

with open(sys.argv[2], 'w', encoding='utf-8', newline='') as f:
    dataWriter = csv.writer(f)

    dataWriter.writerow(
        ['GroupName','GroupId','IpProtocol','FromPort','ToPort','CidrIp/UserIdGroupPairs'])
    security_groups = []
    for _sg in response['SecurityGroups']:
        for ip_perm in _sg['IpPermissions'] + _sg['IpPermissionsEgress']:
            for ip in ip_perm['IpRanges']:
                security_groups.append(
                [_sg['GroupName'],
                    _sg['GroupId'],
                    ip_perm.get('IpProtocol',''),
                    str(ip_perm.get('FromPort',-1)),
                    str(ip_perm.get('ToPort',-1)),
                    ip['CidrIp']])
            for ip in ip_perm['Ipv6Ranges']:
                security_groups.append(
                    [_sg['GroupName'],
                    _sg['GroupId'],
                    ip_perm.get('IpProtocol',''),
                    str(ip_perm.get('FromPort',-1)),
                    str(ip_perm.get('ToPort',-1)),
                    ip['CidrIpv6']])
            for pair in ip_perm['UserIdGroupPairs']:
                security_groups.append(
                    [_sg['GroupName'],
                    _sg['GroupId'],
                    ip_perm.get('IpProtocol',''),
                    str(ip_perm.get('FromPort',-1)),
                    str(ip_perm.get('ToPort',-1)),
                    pair['GroupId']])
        else:
            security_groups.append(
                [_sg['GroupName'],
                _sg['GroupId'],
                '',
                '',
                '',
                ''])
    security_groups.sort(key=lambda x: x[0])
    dataWriter.writerows(security_groups)