マルチアカウント環境でAWS Security Hubの管理を効率化するAWS公式のサンプルソリューションを試してみた

マルチアカウント環境でSecurityHubのコントロール有効化/無効化が楽になります

こんにちは。枡川です。
Security Hubを運用している際、環境特有の理由等で特定コントロールを無効化したい場合があると思います。
しかしマルチアカウント環境で対象アカウントが多い環境では、コントロールを無効化するだけでも一苦労となります。
マルチアカウント環境でのSecurityHubの管理を楽にしてくれるサンプルソリューションがAWS公式リポジトリで紹介されていたので試してみました。

何をしてくれるのか

Security Hubではマスターアカウントとメンバーアカウントを作成して、マルチアカウント管理を行うことが可能です。
この際、マスターアカウントにメンバーアカウントの検出結果を集約することが可能ですが、各アカウントのSecurity Hub管理を楽にしてくれるわけではありません。
もし愚直にコントロールを無効化しようとすれば、各アカウントごとに一つずつコントロールを無効化する必要があります。

今回のサンプルソリューションは、マスターアカウントにSAM(Serverless Application Model)でアプリケーションを作成し、アプリケーション経由でメンバーアカウントのコントロールを無効化/有効化させるものになります。
マスターアカウントでコントロールを無効化したら自動でメンバーアカウントのコントロールも無効になる形です。
DynamoDBに例外設定を持たせることができるので、特定のメンバーアカウントのみ無効化といったことも可能です。
コントロールだけではなく、スタンダート単位(AWS 基礎セキュリティのベストプラクティス v1.0.0等)での有効化/無効化処理も実施することが可能です。
これらの管理についてアカウント毎に設定を行う必要が無く、マスターアカウントのみに設定を入れるだけで良くなるのでかなり楽になります。
スクリプトを組んで複数アカウントのコントロールを一括で無効化することも可能ですが、各時点でコントロール(スタンダード)がどうなっているかを把握するのが難しいですし、開発工数もかかります。
開発の可能性が少なく、その時点での設定はマスターアカウントのSecrurityHubとDynamoDBを見ればわかるというのが良いです。

※Security Hubのマスターアカウント/メンバーアカウントについて、AWS Organizationsと統合することは可能ですが、AWS Organizationsとは別機能のものになります。
今回のソリューションはSecurity Hubのマスターアカウント、メンバーアカウントの関係を使用するので、Organizations環境と統合されていてもされていなくても使用できます。
コントロールを無効化する対象アカウントはSecurity Hubのメンバーアカウントを取得するAPIから取得するのでAWS Organizationsは必須ではありません。
一方でIAMリソースなどを配る際にAWS Organizationsと統合されていた方が楽になります。
Security HubとAWS Organizationsの統合については下記ブログを参照下さい。

※ Control Tower環境の場合は下記ブログに紹介されている通り、カスタマイズソリューションを活用したメンバーアカウントのSecurity Hub管理手法が存在します。

アプリケーションの構成

SAMによって構築されるアプリケーションは下図のようになります。
(引用)aws-samples/aws-security-hub-cross-account-controls-disabler
予めSecurity Hubのコントロールを操作するためのIAMロールをメンバーアカウントに配っておき、マスターアカウントにEventBridge、StepFunctions、Lambda、DynamoDBといったリソースを構築します。
StepFunctionsはマスターアカウントのSecurity Hubへの変更を入れた際、もしくはEventBridge経由でスケジュール実行されます。
SecurityHubの変更を検知するためのイベントパターンは下記です。

{
  "detail-type": ["AWS API Call via CloudTrail"],
  "source": ["aws.securityhub"],
  "detail": {
    "eventSource": ["securityhub.amazonaws.com"],
    "eventName": ["UpdateStandardsControl", "BatchDisableStandards", "BatchEnableStandards"]
  }
}

Security Hubのコントロール変更とDynamoDBの変更の際のみ実行すればスケジュール実行はいらないのではと思いましたが、スケジュール実行することで新たなメンバーアカウントが追加された時にも自動でコントロールを無効化する対象に含むことができています。
StepFunctionsが動く際にSecurityHubのAPIからメンバーアカウントのID一覧を都度取得しているので、追加されたメンバーアカウントに対しても自動で設定が反映されます。
アプリがデプロイされる前に行われていた無効化/有効化設定についても反映させるためにもスケジュール実行させているようです。
StepFunctionsのフロー図も引用します。
.
(引用)aws-samples/aws-security-hub-cross-account-controls-disabler
StepFunctionsは3つのLambda関数から成り立っています。
GetMembersはメンバーアカウントIDの取得と、DynamoDBから例外設定の読み取りを行ないます。
GetMembersで取得したメンバーアカウントに対して1つずつUpdateMemberアカウントを実施し、スタンダードやコントロールの設定を反映させる形になります。
最後にCheckResultで実行結果を確認し、成功した場合はそのまま終了、失敗した場合はAmazon SNS経由でメール通知をする流れとなります。

使ってみた

まずmember-iam-role/template.yamlを使用して、IAMをメンバーアカウントに配ります。
作成するIAMロールでは下記のSecurity Hubを操作する権限を与えます。

  • securityhub:Get*
  • securityhub:List*
  • securityhub:Describe*
  • securityhub:UpdateStandardsControl
  • securityhub:BatchDisableStandards
  • securityhub:BatchEnableStandards

CloudFormations StackSetを使用すると楽に配ることができます。
AWS OrganizationsとSecurity Hubを統合している場合はOUのID指定で配ることもできますし、アカウントIDを指定したStack Setsでも問題ないです。
aws cloudformation create-stack-instancesを使用する場合、Organizationsと統合されているなら--deployment-targets OrganizationalUnitIds=xxxxのように指定して、そうでない場合は--deployment-targets Accounts=["xxxx","yyyy"]のように指定する形になります。
この際、SecurityHubのマスターアカウントで下記コマンドを実行すれば、メンバーアカウントのID一覧を取得することが可能です。

account_list=`aws securityhub list-members | jq '[ .Members[].AccountId ]'`

IAMロールを配ったら、SAMのアプリケーションをSecurity Hubのマスターアカウントにデプロイします。
まず、スクリプトを取得します。

git clone git@github.com:aws-samples/aws-security-hub-cross-account-controls-disabler.git
cd aws-security-hub-cross-account-controls-disabler

Github記載の通り、下記を実行します。
artifact-bucketはSAMが生成したパッケージファイルを格納するS3バケットなので、任意のバケットを指定します。

sam package --template-file UpdateMembers/template.yaml --output-template-file UpdateMembers/template-out.yaml --s3-bucket <artifact-bucket>
aws cloudformation deploy --template-file UpdateMembers/template-out.yaml --capabilities CAPABILITY_IAM --stack-name <stack-name>

デプロイが完了したら、EventBridge、StepFunctions、Lambda、DynamoDBといったリソースが作成されます。
まず、スタンダートそのものの有効化/無効化を確認してみます。
マスターアカウントでもメンバーアカウントでも、Security Hubのセキュリティ基準はAWS 基礎セキュリティのベストプラクティス v1.0.0のみを使用している状態を想定します。
ここで、マスターアカウントでCIS AWS Foundations Benchmark v1.2.0を有効化します。
有効化が完了するとEventBridgeがそれを検知してStepFunctionsが実行され、メンバーアカウントのCIS AWS Foundations Benchmark v1.2.0が有効化されました。
やっぱりAWS 基礎セキュリティのベストプラクティス v1.0.0だけで良いとなった場合は、マスターアカウントでCIS AWS Foundations Benchmark v1.2.0を無効化すれば、自動でメンバーアカウントでも無効化されます。
次に各コントロールを有効化/無効化してみます。
こちらもスタンダートの有効化/無効化と同じでマスターアカウントでコントロールを有効化/無効化したらStepFunctions経由でメンバーアカウントも有効化/無効化される形になります。
理由もつけて無効化した際にはそれも含めてメンバーアカウントに反映させてくれます。
マスターアカウントのSecurity Hubを見に行けば、その時点での設定を一目瞭然なのが良いですし、有効化/無効化をかなりやりやすくなりました。
また、特定アカウントのみ無効化/有効化したいとなればDynamoDBに設定を入れることで例外を作成することもできます。
DynamoDBには下記のように例外設定を入れます。

(引用)aws-samples/aws-security-hub-cross-account-controls-disabler
こちらの設定を入れることで、マスターアカウントで有効化したコントロールを特定のメンバーアカウントでのみ無効化するようなことが可能です。
DynamoDBにはマネジメントコンソール経由で設定を入れても良いですし、下記のようにCLIで設定を入れることも可能です。

aws dynamodb put-item --table-name security-hub-tuning-app-stack-AccountExceptions-xxxxxxxxxxxx --item '{ "ControlId": { "S": "Config.1" }, "Disabled": { "L": [ { "S": "123456789012" } ] }, "DisabledReasod": { "S": "Some_Reason" } }'

複数リージョンでSecurity Hubを有効化している場合について

このサンプルソリューションは各リージョン単位でデプロイするため、複数リージョンでSecurity Hubを使用している場合はそれぞれのリージョンにデプロイする必要があります。
一見面倒に思えますが、リージョン毎に設定を分ける要件はかなりの頻度で発生します。
IAMなどのグローバルリソースは各リージョンで記録すると、重複して記録されることになります。
Configの料金に響きますし、チェックが発生すればSecurity Hubの料金にも影響してきます。
※ Configのグローバルリソースの重複課金については下記ブログが非常にわかりやすいです。

一方でAWS 基礎セキュリティのベストプラクティス v1.0.0を使用していると、すべてのリージョンでグローバルリソースも含めてConfigの記録対象にしていないと下記コントロールに違反します。
[Config.1] This AWS control checks whether the Config service is enabled in the account for the local region and is recording all resources.
グローバルリソースをそのリージョンで記録していない場合は、IAM関連のコントロールも記録が無いので正しくチェックすることができなくなります。(グローバルリソースを記録しているリージョンでは正しく動作する).
そのため、グローバルリソースを特定リージョンのみで記録する際はそのリージョン以外では関連コントロールを無効化するべきで、公式ドキュメントにもそのように記載があります。

以上を踏まえて、Security HubやDynamoDBに対して複数リージョンを対象に修正を行うことができるスクリプトを用意しつつ、設定は各リージョン個別に持っておくと使い勝手が良いと思っています。
AWS 基礎セキュリティのベストプラクティス v1.0.0を複数リージョンで有効化/無効化するためのスクリプトとして下記のようなものが使用できます。

securityhub_AFSBP_modifier.sh

#!/usr/bin/env bash

regions=(
  "ap-northeast-1"
  "ap-northeast-2"
  "ap-northeast-3"
  "ap-south-1"
  "ap-southeast-1"
)
if [ $2 = "ENABLED" ]; then
  echo "AWS 基礎セキュリティのベストプラクティス v1.0.0${1}を有効化します"
elif [ $2 = "DISABLED" ]; then
  echo "AWS 基礎セキュリティのベストプラクティス v1.0.0${1}を無効化します"
  echo "理由:${3}"
else
  echo "ENABLEDかDISABLEDを選択して下さい"
  exit 1
fi

ACCOUNT_ID=`aws sts get-caller-identity --query 'Account' --output text`

for region in ${regions[@]}
do 
  # echo $region
  STANDARD_CONTROL_ARN="arn:aws:securityhub:${region}:${ACCOUNT_ID}:control/aws-foundational-security-best-practices/v/1.0.0/${1}"
  # echo $STANDARD_CONTROL_ARN
  if [ $2 = "ENABLED" ]; then
    aws securityhub update-standards-control --standards-control-arn $STANDARD_CONTROL_ARN --control-status $2 --region $region
  else
    aws securityhub update-standards-control --standards-control-arn $STANDARD_CONTROL_ARN --control-status $2 --disabled-reason $3 --region $region
  fi
done

コントロールをDISABLEDにする際は下記のようにパラメータを渡します。

./securityhub_disabler.sh "EC2.6" "DISABLED" "利用料金を節約するため"

コントロールをENABLEDにする際は下記のようにパラメータを渡します。

$./securityhub_disabler.sh "EC2.6" "ENABLED"

対象リージョンも引数にしても良いかと思いますが、SecurityHubを有効化している全リージョン対象かグローバルリソースを記録しているリージョン以外を対象という2パターンくらいであれば、埋め込んだ上で2パターンのシェルスクリプトを作成してしまっても良いかなと思いました。
例外処理用のDynamoDBを修正するスクリプトも、下記のように複数リージョンに反映可能にしておくと便利だと思います。

dynamodb_modifier.sh

#!/usr/bin/env bash

declare -A table_id=(
  ["ap-northeast-1"]="security-hub-tuning-app-AccountExceptions-vvvvvvvv"
  ["ap-northeast-2"]="security-hub-tuning-app-stack-AccountExceptions-wwwwwwww"
  ["ap-northeast-3"]="security-hub-tuning-app-stack-AccountExceptions-xxxxxxxx"
  ["ap-south-1"]="security-hub-tuning-app-AccountExceptions-yyyyyyyy"
  ["ap-southeast-1"]="security-hub-tuning-app-stack-AccountExceptions-zzzzzzzz"
)

regions=(
  "ap-northeast-1"
  "ap-northeast-2"
  "ap-northeast-3"
  "ap-south-1"
  "ap-southeast-1"
)

tuningData=`cat ./securityhub-tuning.json`

for region in ${regions[@]}
do 
  aws dynamodb put-item --table-name ${table_id[$region]} --item "${tuningData}" --region $region
done

各リージョンの例外設定用DynamoDBテーブル名を取得するために連想配列を使用しているので、その部分を環境毎に記載する必要があります。
また、macで実行した際はbash3系が使用されて連想配列が使えず、エラーになるかもしれませんが、その場合はbrewでbashをインストールし直せば使えます。
シェルスクリプト内でjsonファイルを読み込んでいますが、別ファイルで下記のように記載しておいてシェル内で使用する想定です。

securityhub-tuning.json

{  
  "ControlId": {
    "S": "EC2.7" 
  }, 
  "Disabled": {
    "L": [ 
      { "S": "123456789012" } 
    ] 
  }, 
  "DisabledReason": {
    "S": "クロスリージョンレプリケーションの転送量を考慮して暗号化しないようにしたため"
  } 
}

最後に

マルチアカウント環境で複数のアカウントを対象にして、Security Hubの特定コントロールを無効化するソリューションを試してみました。
アカウントが増えれば増えるほどSecurity Hubの管理が辛くなっていたのが、まとめて扱えるようになるので非常に良いですね。
このソリューションはPython(boto3)とSAMで作成されているので、少し手をいれてEmail通知をSlackにしてみる、特定OU配下のアカウント全体に対して例外設定をできるようにするなどの機能を実現すると、環境によってはより使い勝手が良くなるかもしれません。
このソリューションでSecurity Hubの管理を楽にして、検出結果への対応に多くの時間をかけれるようにしていきたいです!