AWS Security Hub CSPMでセキュリティ基準を有効にすると、さまざまな検出結果が出てきます。中でもS3バケットのSSL通信の強制とライフサイクル設定は、バケットを作る際に忘れがちで設定も面倒なので、未設定のものは自動で修復したいです。
そこで今回は、AWSにどんな自動修復の仕組みがあるのかを調べ、S3の2つのコントロールについて自動修復を実装しました。
全体の流れは以下の通り。
- 対象コントロールの確認
- 自動修復の選択肢
- 修復内容
- 実装
- 動作確認
1. 対象コントロールの確認
Security Hub CSPMでは、チェック項目を「コントロール」と呼び、S3.5 のようなIDが付いています。今回対象にするのは以下の2つ。
| コントロール | 内容 | 重要度 | 不合格になる条件 |
|---|---|---|---|
| S3.5 | TLSを使った通信を必須にする | Medium | バケットポリシーでHTTPリクエストを拒否していない |
| S3.13 | バケットにライフサイクル設定がある | Low | 有効なライフサイクルルールが無い |
S3.13は特定の基準以上の設定が必要というわけではなく、有効なルールが1つでもあれば合格のようです。
2. 自動修復の選択肢
検出 → イベント → 修復処理を組み合わせて作ることが基本のようです。主な選択肢は以下の3つ。
2-1. EventBridge + Lambda
Security Hub CSPMは、新しい検出結果や更新された検出結果を、Amazon EventBridgeに送ります。EventBridgeのルールで対象の検出結果を拾い、AWS Lambdaで修復処理する方式。
2-2. AWS Configの修復アクション
AWS Configには、非準拠と判定されたリソースを修復する「修復アクション」の機能があります。Configルールに、修復手順を定義したAWS Systems Manager Automationのランブックを紐づけて使うようです。
修復は手動/自動を選べて、S3.5向けには、AWSが用意しているランブック(AWSConfigRemediation-RestrictBucketSSLRequestsOnly)をそのまま使えます。
ただし、Security Hubが作ったConfigルールには修復設定を追加できません。この方式を使う場合は、同じルールを自分で別途作る必要があります。
2-3. Automated Security Response on AWS (ASR)
AWSが公開しているソリューションで、2-1の仕組みだけでなくWeb UIや履歴管理, 通知機能などを盛り込んだ完成品として提供しているもの。多くの修復機能が用意されており、手動/自動実行の切り替えも可能。
2-4. 選択肢の比較と今回の方針
上記3つの選択肢をざっくり比べると以下の通り。
| EventBridge + Lambda | Configの修復アクション | ASR | |
|---|---|---|---|
| トリガー | Security Hubの検出結果 | Configルールの評価結果 | Security Hubの検出結果 |
| 修復処理 | 自作 | ランブック (AWS管理 or 自作) | 用意済み |
| Security Hubの基準をそのまま使えるか | 使える | 使えない | 使える |
| 構成の大きさ | 小 | 小 | 大 |
ASRは高機能ですが、今回直したいコントロールは2つだけなので、自動化の対象が増えてきたらASRを検討し、それまではASRを参考に最低限の機能を自分で実装するという方針にしました。今回はEventBridge + Lambdaで作ります。
3. 修復内容
3-1. S3.5 (TLSの強制)
S3.5に合格するには、HTTPのリクエストを拒否するステートメントをバケットポリシーに入れるだけです。以下は公式の修復例になります。
{
"Id": "ExamplePolicy",
"Version":"2012-10-17",
"Statement": [
{
"Sid": "AllowSSLRequestsOnly",
"Action": "s3:*",
"Effect": "Deny",
"Resource": [
"arn:aws:s3:::amzn-s3-demo-bucket",
"arn:aws:s3:::amzn-s3-demo-bucket/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
},
"Principal": "*"
}
]
}
↑のaws:SecureTransportがfalseの時、全ての操作を拒否します。HTTPSでのアクセスには影響しません。
3-2. S3.13 (ライフサイクル設定)
ライフサイクルは、設定によってはオブジェクトが削除されてしまうので、削除はせず、ストレージクラスの移行だけの修復にします。
未設定の全バケットに入れるルールなので、後からデータを使うことになった場合も考え、30日後にIntelligent-Tieringへ移行するルールにしました。Intelligent-Tieringはアクセスに応じて階層を自動で移動し、アクセスされれば高頻度アクセスの階層へ自動で戻ります。
また、128KB未満のオブジェクトはIntelligent-Tieringへ移行されない(移行しても節約にならない)ので、ObjectSizeGreaterThan のフィルタで明示的に除外します。
3-3. ASRの実装と比較
ASRのソースコードはGitHubで公開されているので、S3.5とS3.13の修復がどう実装されているかを確認しました。
| ASR | 今回の実装 | |
|---|---|---|
| S3.5: 追記するステートメント |
Sid: AllowSSLRequestsOnly のDeny |
同じ |
| S3.5: 既存ポリシーの扱い | 残したまま追記 | 同じ |
| S3.5: 追記済みかの確認 | 修復スクリプト内には無い | あり |
| S3.13: ルールの内容 | 128KB超を30日後にIntelligent-Tiering (デフォルト値) | 同じ |
| S3.13: 既存設定の扱い | 確認せずに置き換え | 同じ |
| S3.13: パラメータのカスタム値 | あれば使う | 使わない |
2で記載した通り、基本的にはASRと同じ内容にしています。
4. 実装
CDK(Python)で実装しました。全コードは以下にあります。
(CDKについては過去の記事も参考にしてください。CDKとは?)
4-1. 構成
構成は以下の通り。
Security Hub CSPM (S3.5/S3.13 が不合格)
↓ 検出結果のイベント
EventBridgeルール (コントロールIDと不合格で絞り込み)
↓
Lambda
├─ S3.5 → バケットポリシーにHTTP拒否のステートメントを追記
└─ S3.13 → ライフサイクル設定を置き換え
4-2. EventBridgeルール
# Security Hub CSPMの検出結果のうち、S3.5とS3.13の失敗だけを拾う
rule = events.Rule(
self,
"FailedS3ControlRule",
event_pattern=events.EventPattern(
source=["aws.securityhub"],
detail_type=["Security Hub Findings - Imported"],
detail={
"findings": {
"Compliance": {
"SecurityControlId": ["S3.5", "S3.13"],
"Status": ["FAILED"],
},
"Workflow": {"Status": ["NEW"]},
"RecordState": ["ACTIVE"],
}
},
),
)
rule.add_target(targets.LambdaFunction(remediate_fn))
- コントロールの絞り込みには、
SecurityControlIdを使用 -
Workflow.StatusはNEWだけを対象
(Security HubのオートメーションルールはEventBridgeのルールより先に適用されるので、例外にしたい場合はオートメーションルールで抑制すれば、修復されないはずです)
4-3. Lambda
修復処理の主要部分は以下の通り。
def remediate_s3_5(s3, bucket, partition):
"""S3.5: HTTPリクエストを拒否するステートメントをバケットポリシーに追記"""
try:
policy = json.loads(s3.get_bucket_policy(Bucket=bucket)["Policy"])
except ClientError as e:
if e.response["Error"]["Code"] != "NoSuchBucketPolicy":
raise
# ポリシーが無ければ新しく作る
policy = {"Id": "BucketPolicy", "Version": "2012-10-17", "Statement": []}
# 追記済みなら何もしない (S3はSidの重複を受け付けるため、確認しないと同じステートメントが並ぶ)
if any(s.get("Sid") == "AllowSSLRequestsOnly" for s in policy["Statement"]):
return "skipped (already exists)"
# 既存のステートメントは残したまま追記する
policy["Statement"].append(
{
"Sid": "AllowSSLRequestsOnly",
...
}
)
s3.put_bucket_policy(Bucket=bucket, Policy=json.dumps(policy))
return "remediated"
def remediate_s3_13(s3, bucket, partition):
"""S3.13: ライフサイクル設定を上書きする"""
s3.put_bucket_lifecycle_configuration(
Bucket=bucket,
LifecycleConfiguration={"Rules": [LIFECYCLE_RULE]},
)
return "remediated"
# コントロールIDと修復処理の対応。他のコントロールを追加する場合はここに足す
REMEDIATIONS = {
"S3.5": remediate_s3_5,
"S3.13": remediate_s3_13,
}
S3.13で設定するルールは以下の通り。
# 128KBより大きいオブジェクトを30日後にIntelligent-Tieringへ移行
LIFECYCLE_RULE = {
"ID": "S3.13 auto remediation",
"Status": "Enabled",
"Filter": {"ObjectSizeGreaterThan": 131072},
"Transitions": [{"Days": 30, "StorageClass": "INTELLIGENT_TIERING"}],
}
ASRでは、修復後に検出結果のワークフローステータスをRESOLVED(解決済み)に更新しますが、今回の実装では更新しません。再評価で合格(PASSED)になると、自動で RESOLVEDになるので不要としました。
(上記のように、ASRでは修復処理の外側で二重実行しない仕組みを入れていますが、今回は簡単な構成にしたかったので、修復処理を何度実行されても同じ結果となるようにしました)
5. 動作確認
Security Hubの評価は時間がかかるので、架空のイベントで確認しました。
5-1. ルールの条件
架空のイベントは以下の通り。Lambdaが使う項目だけを入れた最小限のものです。
{
"version": "0",
"id": "00000000-0000-0000-0000-000000000001",
"detail-type": "Security Hub Findings - Imported",
"source": "aws.securityhub",
"account": "<アカウントID>",
"time": "YYYY-MM-DDT00:00:00Z",
"region": "ap-northeast-1",
"resources": [],
"detail": {
"findings": [
{
"AwsAccountId": "<アカウントID>",
"Compliance": { "SecurityControlId": "S3.5", "Status": "FAILED" },
"Workflow": { "Status": "NEW" },
"RecordState": "ACTIVE",
"Resources": [
{ "Type": "AwsS3Bucket", "Id": "arn:aws:s3:::<バケット名>", "Region": "ap-northeast-1" }
]
}
]
}
}
S3.13用はSecurityControlIdをS3.13に変えるだけです。
5-2. Lambdaの動作確認
検証用に以下の2つのバケットを作り、バケット名だけを変えた上記の架空イベントをLambdaに渡しました。
- バケットA: 何も設定なし
- バケットB: Allowのステートメント1つと、適当なライフサイクルルールを設定済み
aws lambda invoke --function-name <関数名> --payload fileb://event-s3-5.json out.json
aws lambda invoke --function-name <関数名> --payload fileb://event-s3-13.json out.json
結果は以下の通り。
| バケット | バケットポリシー | ライフサイクル設定 |
|---|---|---|
| A (設定なし) | 新規作成され、AllowSSLRequestsOnly の1つだけ |
今回のルールが作成された |
| B (設定あり) | 既存のAllowが残ったまま、AllowSSLRequestsOnly が追記 |
既存のルールが消え、今回のルールに置換 |
もし同じイベントが2回届いても、S3.5は以下のようなログを出すだけでポリシーは変わりません。S3.13も上記の通り処理自体が置換なので設定が変わることはありません。
{
"control": "S3.5",
"bucket": "<バケット名>",
"result": "skipped (already exists)"
}
まとめ
Security Hub CSPMのS3.5とS3.13の検出結果を、EventBridge + Lambdaで自動修復する仕組みを実装しました。
- 自動修復は「検出 → イベント → 修復処理」の組み合わせで作る
- 対象が少なければ今回のような自作でいいが、増えてきたらASRを使った方が良さそう
- 修復内容は既存の設定への影響で決める (追記/置換)
- 二重実行を防止してないなら、何度実行しても同じ結果になるよう作る
Security Hubの実際の評価から修復までの通しの動きはまだ確認できていないので、運用の中で確かめたいと思います。
また今回は単一アカウント内の処理を想定してますが、マルチアカウント環境の場合は委任管理者アカウントからメンバーアカウントの修復を行うのが望ましいです。
ASRもそれを想定した実装となっているので、今回の実装をマルチアカウントに展開する場合もASRを参考にして検討したいと思います。