綺麗に死ぬITエンジニア

EC2のセキュリティグループにCloudFrontからしかアクセスを許可しない設定を追加する(改良版)

2015-10-09

※ AWSのLambda, SNS, S3を活用した、本記事のサーバーレス版を後日作成しました! こちらからどうぞ。

本記事投稿日現在、AWSのセキュリティグループの受信許可設定には、IPアドレスの指定および他のセキュリティグループのID指定しか対応していません。つまり、CloudFront経由でしかアクセスさせたくない場合であっても、そのような設定をすることは、現時点ではできません。

そこで今回、定期的に自動でCloudFrontのIPアドレスを調べ、セキュリティグループを設定してくれるシェルスクリプトを作成しました。

今回の記事のスクリプトは、前回の記事の改良版のスクリプトです。根本的な動作について知りたい方は、前回の記事も合わせてご覧ください。

本スクリプトのメリット

このスクリプトを利用することにより、WebサーバへのアクセスがCloudFrontからのみに絞られるため、外部からWebサーバ本体(EC2)のIPアドレスを特定することが困難となり、サーバ本体に対するHTTP/S経由以外の攻撃を強力に軽減することができます。

SSH経由で不正アクセスを試みようとも、IPアドレスがわからなければ攻撃などできません。

よって、悪意のある攻撃者に、意図的に狙い撃ちされるリスクが軽減されます。

スクリプトの設定

以下のように設定してください。

# スクリプトを動かす為のディレクトリ・ファイル群を作成します(ここでは、/root/aws_cli以下に作成します)
~ $ pwd
/root
~ $ mkdir aws_cli    # ここは任意の名称で構いません。ただし、以下のディレクトリは名称を合わせてください
~ $ cd aws_cli
~/aws_cli $ mkdir ip-range    # IP範囲設定を保存するディレクトリの作成
~/aws_cli $ touch ip-range/HEAD   # 現在の許可IP範囲設定を保存するファイルの作成
~/aws_cli $ mkdir log    # ログを保存するディレクトリの作成
~/aws_cli $ vi put-ip-range-of-cloudfront-to-security-group.sh   #本体スクリプトを作成

作成するスクリプト「put-ip-range-of-cloudfront-to-security-group.sh」の内容は以下です。

#!/bin/bash

# 設定値の指定(パラメータの変更はここで行います)
SECURITY_GROUP_ID="sg-********"
PROTOCOL="tcp"
PORT="80"

# 作業ディレクトリへ移動
cd `dirname "${0}"`

# IP範囲をJSON形式で取得
IP_RANGE=`curl -s https://ip-ranges.amazonaws.com/ip-ranges.json`

# syncToken(UNIX エポック時刻形式での公開時刻)を取得
SYNC_TOKEN=`echo ${IP_RANGE} | jq -r '.syncToken'`

# 現在セキュリティグループで許可設定しているIP範囲のsyncTokenを取得
HEAD=`cat ip-range/HEAD`

# 新しく取得したIP範囲のsyncTokenと許可設定しているIP範囲のsyncTokenが違ったら
if [ "${SYNC_TOKEN}" != "${HEAD}" ]; then

    # syncTokenを新しいものに変更
    echo $SYNC_TOKEN > ip-range/HEAD

    # JSONファイルを保存
    echo $IP_RANGE > ip-range/${SYNC_TOKEN}.json

    # IP_RANGEをJSON形式からCLOUDFRONTで利用中のIPアドレスの改行区切りに変換
    IP_RANGE=`echo $IP_RANGE | jq -r '.prefixes[] | if .service == "CLOUDFRONT" then .ip_prefix else empty end'`

    # 前回のsyncTokenの値が空でなければ
    if [ -n "${HEAD}" ]; then

        # 前回のIP範囲を取得(IPアドレスの改行区切り)
        PREV_IP_RANGE=`cat ip-range/${HEAD}.json | jq -r '.prefixes[] | if .service == "CLOUDFRONT" then .ip_prefix else empty end'`

        # 前回のIP範囲との差分を取得する
        DIFF=`diff <(echo "${PREV_IP_RANGE}") <(echo "${IP_RANGE}")`

        # 今回のIP範囲にしかないものを追加する
        for i in `echo "$DIFF" | awk '{if($1==">")print $2}'`; do
            aws ec2 authorize-security-group-ingress --group-id ${SECURITY_GROUP_ID} --protocol ${PROTOCOL} --port ${PORT} --cidr ${i} &&
            echo `date "+%Y/%m/%d %H:%M:%S"` "Added ${i} to the security group ${SECURITY_GROUP_ID}. Protocol=${PROTOCOL} Port=${PORT}" >> log/job.log
        done

        # 前回のIP範囲にしかないものを削除する
        for i in `echo "$DIFF" | awk '{if($1=="<")print $2}'`; do
            aws ec2 revoke-security-group-ingress --group-id ${SECURITY_GROUP_ID} --protocol ${PROTOCOL} --port ${PORT} --cidr ${i} &&
            echo `date "+%Y/%m/%d %H:%M:%S"` "Removed ${i} from the security group ${SECURITY_GROUP_ID}. Protocol=${PROTOCOL} Port=${PORT}" >> log/job.log
        done

    # 前回のsyncTokenの値が空なら(初回実行時)
    else

        # 新しく取得したIP範囲を全てセキュリティグループの許可設定に追加する
        for i in ${IP_RANGE}; do
            aws ec2 authorize-security-group-ingress --group-id ${SECURITY_GROUP_ID} --protocol ${PROTOCOL} --port ${PORT} --cidr ${i} &&
            echo `date "+%Y/%m/%d %H:%M:%S"` "Added ${i} to the security group ${SECURITY_GROUP_ID}. Protocol=${PROTOCOL} Port=${PORT}" >> log/job.log
        done

    fi

fi

4〜6行目の設定値については、それぞれの環境に合わせて変更してください。

また、スクリプト内部でjqコマンドを利用しています。インストールされていない場合は、以下のコマンドでインストールしてください。(Amazon Linux)

~ $ yum -y install jq

また、awsコマンドを実行しているため、aws configureで初期設定を済ませておいてください。こちらに詳しい解説があります。

以上でファイルの配置は終了です。

動作解説

本スクリプトは、以下のような挙動をします。

  1. AWSの使用IPアドレス範囲を示すJSONファイル(https://ip-ranges.amazonaws.com/ip-ranges.json)を取得する
  2. 取得したJSONファイルのsyncToken(公開時刻に基づく数値)の値を参照し、前回取得分(ip-range/HEADに格納されている)と同じならば何もしない。異なった場合、次に進む
  3. 新しいsyncTokenを保存(ip-range/HEADへ)
  4. 1で取得したJSONファイルを、ip-rangeディレクトリに保存
  5. 前回の情報が見つからなければ、今回取得したCloudFrontのIPアドレス範囲を、全てセキュリティグループの許可対象に追加し、ここで終了する。前回の情報があれば、次に進む
  6. 前回許可したCloudFrontのIPアドレス範囲と今回取得したCloudFrontのIPアドレス範囲の差分を計算
  7. 今回取得分にしか含まれないIPアドレス範囲を、セキュリティグループの許可対象に追加
  8. 前回取得分にしか含まれないIPアドレス範囲を、セキュリティグループの許可対象から削除

初回実行

初回実行時は、上記手順5.により、正常に稼働するようになっています。下記のように入力し、シェルスクリプトを実行させ、実際にセキュリティグループの設定が正しく動作しているか確認してください。

~ $ /root/aws_cli/put-ip-range-of-cloudfront-to-security-group.sh

cronの設定

問題がなければ、AWSのIPアドレス範囲変更時に自動で新IP範囲を取得・設定するように、本スクリプトを定期的に実行するようcronの設定を行います。

~ $ crontab -e
15 * * * * /root/aws_cli/put-ip-range-of-cloudfront-to-security-group.sh

ここでは、1時間に1回実行するように設定しています。

ログの閲覧

セキュリティグループへの追加&削除の操作情報は、全てログに保存されます。エラー情報は保存されません。実際に操作し成功した情報のみです。

以下のディレクトリに保存するようになっていますので、実際に正常動作しているかどうかの確認にご利用ください。

~/aws_cli $ less log/job.log

前回の記事からの変更点

前回の記事から、以下の部分について改良を加えました。

  • 前回取得分と今回取得分のIPアドレス範囲の差分を計算し、セキュリティグループへの操作を最小限に抑えるように変更
  • 動作ログを出力するように変更

前回のスクリプトを数日間運用してみたのですが、AWS全体のIPアドレス範囲の更新があっても、CloudFront自体のIPアドレス範囲は変わらないことが多く、無駄な処理が多いことに気づきました。なので、CloudFrontのIPが変わったときにだけ、変わったIPアドレスの分だけ最小限にリクエストを送るように修正しました。おかげで、実行速度も大分改善されたと思います。

なお、差分の計算にはdiffコマンドを使い、極力処理や記述が肥大化しないようにも努めました。

まとめ

結構面倒な設定が必要ですが、セキュリティグループに「0.0.0.0/0」がないのは気持ちがいいです。

ただ、この辺りの指定は将来AWS側で簡単に設定できるようにしてくれそうですけどね。

皆さんも是非、設定してみてください。また、スクリプトに関して、バグや改善点等ありましたら、コメントお願いします。

※ AWSのLambda, SNS, S3を活用した、本記事のサーバーレス版を後日作成しました! こちらからどうぞ。