CT.APPSYNC.PV.1
コントロールの説明
AWS AppSync GraphQL API をプライベート可視性で構成することを要求する。
AWS AppSync は GraphQL API を提供するマネージドサービスである。API の可視性には以下の 2 種類がある。
| 可視性 | 説明 |
|---|---|
GLOBAL(デフォルト) | インターネットからエンドポイントにアクセス可能 |
PRIVATE | VPC エンドポイント経由でのみアクセス可能 |
--visibility を指定せずに API を作成すると GLOBAL になる(実機確認済み)。つまり、意識的に PRIVATE を指定しない限りパブリックな API が作成される。
S3 の BPA のようなアカウントレベルでデフォルトをブロックする設定は AppSync には存在しない。本コントロール(SCP)が唯一のガードレールとなる。ただし、AppSync の可視性は作成時にのみ設定でき、作成後に変更する API は存在しない(UpdateGraphqlApi に visibility パラメータがないことを実機確認済み)。そのため、CreateGraphqlApi をブロックする本コントロールだけでパブリック API の作成を完全に防止できる。
本コントロールは SCP により、GLOBAL(パブリック)な API の新規作成をブロックする。PRIVATE な API の作成は許可される。既存のパブリック API には遡及しない(SCP は API コールをブロックするだけで、既存リソースの設定は変更しない)。
Security Hub CSPM には AppSync の可視性をチェックするコントロールは存在しないため、本コントロールによる予防が唯一の対策となる。
検証環境
本記事のコマンドは、--profile 指定がない場合は Workload アカウントで実行する。事前に export AWS_PROFILE=Workload でデフォルトを設定しておくと便利。
検証の流れ
flowchart LR
A[1. 事前準備:<br>パブリック API 作成] --> B[2. CT.APPSYNC.PV.1<br>有効化]
B --> C[3. 既存 API<br>維持を確認]
B --> D[4. 新規パブリック API<br>作成拒否を確認]
B --> E[5. 新規プライベート API<br>作成成功を確認]
C & D & E --> F[6. CloudTrail<br>で確認]
F --> G[7. CT.APPSYNC.PV.1<br>無効化]
G --> H[8. パブリック API<br>作成成功を確認]
H --> I[9. クリーンアップ]
結果
- コントロールの有効化前、AppSync GraphQL API を
visibility: GLOBAL(パブリック)で作成できることを確認できた - コントロールの有効化後、既存のパブリック API は維持されることを確認できた。SCP は API コールをブロックするだけで既存リソースには遡及しない
- 有効化中に
visibility: GLOBALで新規 API を作成しようとすると、explicit deny in a service control policyで拒否されることを確認できた - 有効化中に
visibility: PRIVATEで新規 API を作成することは可能であることを確認できた - 拒否されたイベントが CloudTrail に記録されることを確認できた(ただし
errorMessageは null。他の SCP コントロールではerrorMessageが含まれていた) - コントロールの無効化後、再び
visibility: GLOBALで API を作成できることを確認できた
1. 事前準備
パブリック(GLOBAL)な AppSync API を作成
コントロール有効化前に、パブリックな API が作成できることを確認する。
aws appsync create-graphql-api \
--name <コントロール有効化前 API 名> \
--authentication-type API_KEY \
--visibility GLOBAL \
--region ap-northeast-1{
"graphqlApi": {
"name": "<コントロール有効化前 API 名>",
"apiId": "<API ID>",
"visibility": "GLOBAL",
...
}
}visibility: GLOBAL で作成成功。
2. CT.APPSYNC.PV.1 有効化
コントロールが未有効化であることを確認
aws controltower list-enabled-controls \
--target-identifier <対象 OU ARN> \
--query 'enabledControls[?contains(controlIdentifier, `br8o4lzgtvz3he7f8sxg76k7q`)]' \
--profile Master \
--region ap-northeast-1[]コントロールを有効化
aws controltower enable-control \
--control-identifier arn:aws:controlcatalog:::control/br8o4lzgtvz3he7f8sxg76k7q \
--target-identifier <対象 OU ARN> \
--profile Master \
--region ap-northeast-1{
"arn": "<有効化 ARN>",
"operationIdentifier": "<オペレーション ID>"
}有効化の完了を確認
aws controltower get-control-operation \
--operation-identifier <オペレーション ID> \
--query 'controlOperation.{operationType:operationType,status:status}' \
--profile Master \
--region ap-northeast-1{
"operationType": "ENABLE_CONTROL",
"status": "SUCCEEDED"
}3. 既存 API の維持を確認
SCP は API コールをブロックするだけで既存リソースには遡及しない。ステップ 1 で作成したパブリック API が残っていることを確認する。
aws appsync get-graphql-api \
--api-id <API ID> \
--query 'graphqlApi.{name:name,apiId:apiId,visibility:visibility}' \
--region ap-northeast-1{
"name": "<コントロール有効化前 API 名>",
"apiId": "<API ID>",
"visibility": "GLOBAL"
}既存のパブリック API は維持されている。
4. 新規パブリック API の作成拒否を確認
aws appsync create-graphql-api \
--name <コントロール有効化後 API 名> \
--authentication-type API_KEY \
--visibility GLOBAL \
--region ap-northeast-1An error occurred (AccessDeniedException) when calling the CreateGraphqlApi operation: ...パブリック(GLOBAL)な API の作成が SCP によりブロックされる。
5. 新規プライベート API の作成成功を確認
aws appsync create-graphql-api \
--name <プライベート API 名> \
--authentication-type API_KEY \
--visibility PRIVATE \
--region ap-northeast-1{
"graphqlApi": {
"name": "<プライベート API 名>",
"apiId": "<プライベート API ID>",
"visibility": "PRIVATE",
...
}
}プライベート(PRIVATE)な API は作成可能。
6. CloudTrail で確認
CloudTrail で拒否イベントを確認
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=CreateGraphqlApi \
--start-time <検索開始時刻> \
--end-time <検索終了時刻> \
--region ap-northeast-1 \
--query 'Events[].CloudTrailEvent' \
--output text | jq 'select(.errorCode != null) | {eventName, errorCode, errorMessage}'{
"eventName": "CreateGraphqlApi",
"errorCode": "AccessDenied",
"errorMessage": null
}拒否イベントが記録されている。errorMessage は null(他の SCP コントロールでは errorMessage に SCP ARN 等が含まれる場合があるが、AppSync では null)。
CloudWatch Logs(Organization Trail)で確認
aws logs start-query \
--log-group-name <Organization Trail ログループ名> \
--start-time <検索開始時刻(UNIX)> \
--end-time <検索終了時刻(UNIX)> \
--query-string 'fields @timestamp, eventName, errorCode, errorMessage | filter eventName = "CreateGraphqlApi" and errorCode != ""' \
--profile Master \
--region ap-northeast-1{
"queryId": "<クエリ ID>"
}aws logs get-query-results \
--query-id <クエリ ID> \
--profile Master \
--region ap-northeast-1{
"results": [
[
{"field": "@timestamp", "value": "<タイムスタンプ>"},
{"field": "eventName", "value": "CreateGraphqlApi"},
{"field": "errorCode", "value": "AccessDenied"}
]
],
"status": "Complete"
}拒否イベントが記録されている。errorCode: AccessDenied が確認できるが、errorMessage フィールドは存在しない(AppSync 固有の挙動。他の SCP コントロールでは errorMessage に SCP ARN 等が含まれる)。
errorMessage に explicit deny in a service control policy が含まれないため、errorMessage ベースの SCP 拒否検知フィルタでは AppSync の拒否イベントを検知できない。errorCode と eventName の組み合わせで検知する必要がある。なお、CT.S3.PV.4 や CT.STS.PV.1 でも errorMessage に詳細が含まれないが、これらは組織外アカウントからのリクエストであるため省略される(AWS 公式ブログに明記)。CT.APPSYNC.PV.1 は組織内アカウントからのリクエストであるにもかかわらず errorMessage が null であり、AppSync 固有の挙動と考えられる。7. CT.APPSYNC.PV.1 無効化
aws controltower disable-control \
--control-identifier arn:aws:controlcatalog:::control/br8o4lzgtvz3he7f8sxg76k7q \
--target-identifier <対象 OU ARN> \
--profile Master \
--region ap-northeast-1{
"operationIdentifier": "<オペレーション ID>"
}aws controltower get-control-operation \
--operation-identifier <オペレーション ID> \
--query 'controlOperation.{operationType:operationType,status:status}' \
--profile Master \
--region ap-northeast-1{
"operationType": "DISABLE_CONTROL",
"status": "SUCCEEDED"
}8. パブリック API 作成成功を確認
コントロール無効化後、再びパブリック API が作成できることを確認する。
aws appsync create-graphql-api \
--name <コントロール無効化後 API 名> \
--authentication-type API_KEY \
--visibility GLOBAL \
--region ap-northeast-1{
"graphqlApi": {
"name": "<コントロール無効化後 API 名>",
"apiId": "<API ID>",
"visibility": "GLOBAL",
...
}
}9. クリーンアップ
aws appsync delete-graphql-api \
--api-id <コントロール有効化前 API ID> \
--region ap-northeast-1
(出力なし)aws appsync delete-graphql-api \
--api-id <プライベート API ID> \
--region ap-northeast-1
(出力なし)aws appsync delete-graphql-api \
--api-id <コントロール無効化後 API ID> \
--region ap-northeast-1
(出力なし)