【AWSインシデント対応】Amazon EC2インスタンスの封じ込め方法

記事タイトルとURLをコピーする

こんにちは!イーゴリです。

シナリオ

Amazon GuardDutyから「CryptoCurrency:EC2/BitcoinTool.B!DNS」のようなクリティカル通知を受けました。

対策方法

対策の流れは下記となります。

  1. ステップ1: 環境変更の前に、EC2 インスタンスからメタデータをキャプチャします。
  2. ステップ2:インスタンスの停止及び終了保護を有効にして、EC2 インスタンスを誤って停止/終了しないように保護します※1
  3. ステップ3:EC2 インスタンスを隔離します。
  4. ステップ4:EC2 インスタンスをあらゆる AWS Auto Scaling グループ及び関連する Elastic Load Balancingから切り離します。
  5. ステップ5:EC2 インスタンスにアタッチされた Amazon EBS データボリュームのスナップショットを作成し、取得したスナップショットをフォレンジック目的のアカウントに移動し、可能であれば、移動したスナップショットをフォレンジック用のAWSアカウントで複製します※2 。その後、スナップショットのコピーのフォローアップ調査を行うか、必要に応じて社外の専門家に調査を依頼します。
  6. ステップ6:EC2 インスタンスに対して調査のために隔離済みとしてのタグを付与します(任意)。

なお、GuardDuty Malware Protectionが有効になっている場合、 GuardDuty Malware Protectionによって取得された Amazon EBS スナップショットを保持することはできます。デフォルトでは、スナップショットはスキャン完了後数分後に削除され、スキャンが完了しなかった場合は 24 時間後に削除されますが、スナップショットが削除されないように設定することもできます。

※1 EC2を再起動してしまうと、メモリ上にある情報が失われるため、メモリダンプを取得しない限り、または調査が続いている間は、EC2を停止してはいけません。ただし、場合によっては「シャットダウンを行う」という判断が適切な場合もあります。ただし、ステップ3でEC2を分離しているため、他のシステムへの影響は基本的に想定されていません(この記事では理論的な説明にとどまりますので、必ずお客様ご自身で環境への影響を慎重に評価してください)。そのため、基本的にはEC2を起動したままにしておく方が良いと思います。

※2 ミスによる調査対象を書き込みモードでマウントしてしまう(読み取りモードでのマウント忘れなど)可能性があるため、オリジナルEBSを残したほうがよいです。

上記のステップをご覧いただければ、対応方針がおおよそ理解できるようになったかと思います。しかし、「ステップ3」のEC2インスタンスの隔離方法について十分に理解していない場合、より深刻な問題につながる可能性があります。そこで、この記事では、EC2インスタンスの隔離方法(Amazon EC2インスタンスの封じ込め)について詳しく説明したいと思います。

Amazon EC2 インスタンスの隔離方法

主に2つのセキュリティレベルがあります。

  • ①サブネットレベル:ネットワークACL (以下、NACL)
  • ②インスタンスレベル:セキュリティグループ

NACLとセキュリティグループの比較については、以下のページに詳しく書かれていますが、この記事では、NACLおよびセキュリティグループをAmazon EC2 インスタンスの隔離の観点から解説します。

docs.aws.amazon.com

ネットワークACLでのトラフックブロック

隔離専用のサブネットを作ったとしても、AWSの仕様上ではインスタンスを異なるサブネットに移動することはできません。また、ネットワークACL (NACL)はサブネット全体に適用されるため、特定のインスタンスだけをターゲットにすることはできません。

上記の理由から、サブネットレベルはインスタンスの隔離には適していません。※3

※3 特定のIP/CIDRから対象サブネット内のすべてのリソースに対してトラフィックをブロックしたい場合、NACLを使用する方が効果的ですが、今回の記事のシナリオには直接関係しないため、省略します。

セキュリティグループでのトラフィックブロック

一方、インスタンスレベル(=セキュリティグループ)はインスタンスの隔離に適しているため、こちらについて詳しく説明します。

通常、クラウドでの対策手段としては、インバウンド/アウトバウンドルールのないセキュリティグループに差し替える方法が一般的ですが、追跡されている接続と追跡されていない接続によって動作が異なります。以下で詳細を説明します。

セキュリティグループは接続追跡を使用してトラフィックの送受信に関する情報を管理します。つまり、ステートフルな接続が可能ということです。ただし、すべてのトラフィックが追跡されるわけではなく、例外としてICMPトラフィックなど一部のケースでは常に追跡されます。

  • 追跡されている接続 → 送信元および送信先に具体的なIPアドレスやCIDR(例:192.168.1.15/32, 192.168.1.0/24)が指定されている場合、追跡されている接続となります。
    追跡されている接続では、インバウンド/アウトバウンドのルールを削除や変更しても、既存の接続通信は中断されません

  • 追跡されていない接続 → 送信元および送信先で全トラフィックを許可している場合 (例:インバウンド:0.0.0.0/0、特定のポート(HTTPSなど)とアウトバウンド:0.0.0.0/0 0-65535ポート及びその逆のインバウンド、アウトバウンドルール)、追跡されていない接続となります。
    追跡されていない接続では、インバウンド/アウトバウンドルールを削除や変更すると、その通信はすぐに中断されます

インバウンドルールの例:

追跡接続 プロトコルタイプ ポート番号 ソースIP
TCP 22 (SSH) / 3389 (RDP) XXX.XXX.XXX.XXX/32
X TCP 443 (HTTPS) 0.0.0.0/0 (IPv4) or ::/0 (IPv6)
ICMP All 0.0.0.0/0

アウトバウンドルールの例:

追跡接続 プロトコルタイプ ポート番号 ソースIP
All All 0.0.0.0/0 (IPv4) or ::/0 (IPv6)

詳しくは以下の記事をご参照ください。 docs.aws.amazon.com

上記の説明のため、接続を追跡されている状態の場合、まずは接続を追跡されていない状態に変更してからすべてのルールを削除しないといけません。

  • ステップ1:具体的なIP/CIDR指定のルール(追跡されている接続)を、すべて許可するルールに変更する(追跡されていない接続に変える)。
  • ステップ2:すべてのインバウンド/アウトバウンドルールを削除する。

ただし、上記のパターンを使うと、緊急作業ではミスが起こりやすいため、別途で2つのセキュリティグループを事前に準備したほうが良いです。1つ目は全許可のルールのセキュリティグループ、2つ目は何のルールもない(=全禁止)のセキュリティグループです。ただし、全許可のルールを持つセキュリティグループを環境にそのまま配置するのは危険なため、下記のスクリプトを作成しました。

自動化(スクリプト編)

Amazon EC2インスタンスの分離の自動化

本番の緊急対応時には焦ることが多いため、操作ミスを防ぎ、対応時間を短縮するために事前に手順書を整備し、自動化可能なパターンを作成しておくことが望ましいです。

特にミスが発生しやすいのは、ステップ2、3、および5だと考えられるため、事前にスクリプト化します。

以下に、ステップ2および3に対応するスクリプトを示します。

ステップ2:インスタンスの停止及び終了保護を有効にして、EC2 インスタンスを誤って停止/終了しないように保護します
ステップ3:EC2 インスタンスを隔離します。

スクリプトの使用方法:

sh <スクリプト名>.sh <インスタンスID>

スクリプトの動作説明:

  1. 対象インスタンスの保護を有効化します。
  2. インスタンスのDescribe情報(変更前)を取得し、JSONファイルに保存する。
  3. VPC IDを取得し、対象のVPCで2つのセキュリティグループを作成します(①1つ目はすべてのトラフィックをどこからでも許可するルール、②2つ目はすべてのトラフィックをブロックするルールを設定します)。
  4. 対象インスタンスから既存のセキュリティグループを外して、SG①に差し替えます。
  5. 1秒間待機し、SG①からSG②に差し替えます。
  6. SG①に通常使用してはいけないルールが入っているため、今後のオペレーションミスを防ぐためSG①を削除します。

注意点:この記事のスクリプトについては、一切の責任を負えませんので、ご自身で十分にご検証した上で、このスクリプトを使ってもよいかご判断ください。

#!/bin/bash

# スクリプトが引数としてInstance IDを受け取ることを確認
if [ "$#" -ne 1 ]; then
    echo "使用方法: $0 <Instance ID>"
    exit 1
fi

INSTANCE_ID=$1

# インスタンス保護を有効にする
aws ec2 modify-instance-attribute --instance-id $INSTANCE_ID --attribute disableApiTermination --value true

# インスタンスのDescribe情報を取得し、JSONファイルに保存
aws ec2 describe-instances --instance-ids $INSTANCE_ID --output json >ec2-describe.json

# VPC IDを取得
VPC_ID=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID --query "Reservations[0].Instances[0].VpcId" --output text)

# DenyAllSGセキュリティグループの存在を確認
SG_ID2=$(aws ec2 describe-security-groups --filters Name=group-name,Values=DenyAllSG --query "SecurityGroups[0].GroupId" --output text)
if [ -z "$SG_ID2" ]; then
    # 存在しない場合、DenyAllSGセキュリティグループを作成
    SG_ID2=$(aws ec2 create-security-group --group-name DenyAllSG --description "Deny all traffic" --vpc-id $VPC_ID --query 'GroupId' --output text)
fi

# DenyAllSGセキュリティグループからすべてのルールを削除
aws ec2 revoke-security-group-ingress --group-id $SG_ID2 --protocol all --port all --cidr 0.0.0.0/0 2>/dev/null
aws ec2 revoke-security-group-egress --group-id $SG_ID2 --protocol all --port all --cidr 0.0.0.0/0 2>/dev/null

# AllowAllSGセキュリティグループを作成
SG_ID1=$(aws ec2 create-security-group --group-name AllowAllSG --description "Allow all traffic" --vpc-id $VPC_ID --query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress --group-id $SG_ID1 --protocol -1 --port -1 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-egress --group-id $SG_ID1 --protocol -1 --port -1 --cidr 0.0.0.0/0

# インスタンスのセキュリティグループをAllowAllSGに変更
aws ec2 modify-instance-attribute --instance-id $INSTANCE_ID --groups $SG_ID1

# 1秒待機
sleep 1

# インスタンスのセキュリティグループをDenyAllSGに変更
aws ec2 modify-instance-attribute --instance-id $INSTANCE_ID --groups $SG_ID2

# AllowAllSGセキュリティグループを削除
aws ec2 delete-security-group --group-id $SG_ID1

# ログの出力
echo "インスタンス $INSTANCE_ID は正常に更新されました。"
echo "対象VPC ID: $VPC_ID"
echo "作成されたセキュリティグループ: $SG_ID1 (削除されました), $SG_ID2 (DenyAllSGは適用済み)"

Amazon EC2 インスタンスのAMIの自動取得

実行コマンド:sh <以下のスクリプト名>.sh <インスタンスID>
結果:作成されたAMIとスナップショットの名前は「NameTag+取得年月日」になります。(例:Bastion-20241112)

別のAMI名を設定したい場合、以下のようにインスタンスIDの後ろにAMI名を入れると、指定した名前になります。
例:sh <スクリプト名>.sh <インスタンスID> <AMI/スナップショット名>

注意点:この記事のスクリプトについては、一切の責任を負えませんので、ご自身で十分にご検証した上で、このスクリプトを使ってもよいかご判断ください。

#!/bin/bash

# 引数の確認
if [ -z "$1" ]; then
    echo "使用方法: $0 <インスタンスID>"
    exit 1
fi

INSTANCE_ID="$1"

# インスタンス名の取得
INSTANCE_NAME=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" \
    --query 'Reservations[0].Instances[0].Tags[?Key==`Name`].Value' --output text)

# インスタンス名の取得結果を確認
if [ -z "$INSTANCE_NAME" ]; then
    echo "インスタンスID $INSTANCE_ID に対応する名前が取得できませんでした。"
    exit 1
fi

# 現在の日付をYYYYMMDD形式で取得
TODAY=$(date +"%Y%m%d")

# AMI名の設定(第二引数が存在する場合はそれを使用)
if [ -n "$2" ]; then
    AMI_NAME="$2"
else
    BASE_AMI_NAME="${INSTANCE_NAME}-${TODAY}"
    AMI_NAME="$BASE_AMI_NAME"
fi

SUFFIX=1

# ユニークなAMI名が見つかるまでループ
while true; do
    # AMIの作成を試みる
    AMI_ID=$(aws ec2 create-image --instance-id "$INSTANCE_ID" \
        --name "$AMI_NAME" \
        --tag-specifications "ResourceType=image,Tags=[{Key=Name,Value=${AMI_NAME}}]" \
        "ResourceType=snapshot,Tags=[{Key=Name,Value=${AMI_NAME}}]" \
        --no-reboot --query 'ImageId' --output text 2>&1)

    # AMI作成結果の確認
    if [[ "$AMI_ID" == ami-* ]]; then
        echo "AMIが作成されました: ID $AMI_ID, 名前 $AMI_NAME"
        break
    elif [[ "$AMI_ID" == *"InvalidAMIName.Duplicate"* ]]; then
        # 名前の重複が発生した場合、サフィックスを追加
        SUFFIX=$((SUFFIX + 1))
        AMI_NAME="${BASE_AMI_NAME}_${SUFFIX}"
    else
        # その他のエラーが発生した場合、エラーメッセージを表示して終了
        echo "AMI作成中にエラーが発生しました: $AMI_ID"
        exit 1
    fi
done

#echo "AMIが作成されました: ID $AMI_ID, 名前 ${INSTANCE_NAME}-${TODAY}"

# AMIの作成完了を待機
echo "AMIの作成完了を待っています..."
aws ec2 wait image-available --image-ids "$AMI_ID"

echo "プロセスが正常に完了しました。"

緊急対応時は焦りから誤りを起こす可能性が高いため、できる限り緊急作業は自動化やスクリプト化を推奨します。

Amazon EC2インスタンスの封じ込めの完全自動化の一例

何もないよりは上記のスクリプトがあったほうが確実に役立つと思いますが、上記のスクリプトの紹介だけだと半自動化になっているため、24/7(年中無休)の体制に依存することになります。せめて一次対策(ステップ1〜ステップ6)を自動化する設計を行うべきではないかと思います。以下の方法のほうがよりよいと思います。

イメージ図

必要なコンポーネント

以下の記事ではセキュリティコンポーネントについて詳しく説明していますが、今回の記事では上記のイメージ図に基づき必要なコンポーネント(AWSサービス)を紹介します。

blog.serverworks.co.jp

最低限として以下の基本セキュリティコンポーネント(AWSサービス)を有効にします。

  • AWS CloudTrail
  • AWS Config
  • VPC Flow Logs → 異常検知のため
  • DNS Logs → 異常検知のため
  • Amazon GuardDuty (保護プランを含む) → 異常検知のため+異常が検知され、対象スナップショットがS3に保存されます。そのスナップショットを別枠の対応としてフォレンジック用のアカウントに共有し、調査を行います。

上記のサービスを活用し、一次対応の処理として以下のサービスを設定します。

  • Amazon EventBridge
  • AWS Lambda
  • Amazon SNS
    → アラートが発生したら、EventBridgeがFindingsの種類に応じたルールを基に、適切なRunbookを実行するようにLambdaを呼び出します。同時に、Amazon SNS経由でセキュリティチームへ通知を送ります。

Runbookの一例としては、上記の説明の通り、「EC2を隔離し、タグをつける」などです。

様々な設計があると思いますが、案の1つとして考えてみました。

以上、御一読ありがとうございました。

本田 イーゴリ (記事一覧)

カスタマーサクセス部

・2024 Japan AWS Top Engineers (Security)
・AWS SAP, DOP, SCS, DBS, SAA, DVA, CLF
・Azure AZ-900
・EC-Council CCSE

趣味:日本国内旅行(47都道府県制覇)・ドライブ・音楽