こんにちは。X(クロス)イノベーション本部 ソフトウェアデザインセンター セキュリティグループの耿です。
2022年12月始めに AWS CDK の GitHub リポジトリの wiki に公開された Security And Safety Dev Guide を読んでみました。CDKアプリをデプロイする時の権限と、CDKアプリ内で作成する権限の両方について、管理方法と推奨事項が書かれています。
この記事ではざっくりとした要約と感想を筆者の観点で書いていきます(分かりやすさのために、幾分か内容の再構築や意訳が入っています)。翻訳記事ではないので、正確で詳細な内容は元のドキュメントをご確認ください。
https://github.com/aws/aws-cdk/wiki/Security-And-Safety-Dev-Guide
イントロダクション
<ざっくり要約>
CDKはあらゆるAWSサービスを構築・設定できる強力なツールで、効率的な開発に寄与する一方、組織はコンプライアンスやコストコントロールの目的で開発者の権限を制限したい場合もあり、セキュリティと生産性はしばしば競合する。このドキュメントでは開発者の生産性を損なうことなく、大半のセキュリティに関するユースケースを満たすだろう推奨事項を紹介する。
<感想>
CDKに限らず、組織内のセキュリティ施策が開発を阻害するものとして捉えられる傾向があるのは、セキュリティ担当と開発担当で見ている景色が違うからだろうと思います。セキュリティ担当は最悪の事態を想像して対策を提示するのに対して、開発担当は普段の日々の開発を楽にすることにより価値を置きます。セキュリティ担当の立場としては、開発担当の気持ちを理解し、原理主義にならずに対話型でセキュリティ対策を考える状態を目指したいと考えています。ちなみに筆者が所属するセキュリティを担当するグループでは、開発側の気持ちを理解する方法の一つとして自ら社内向けにシステム開発も行っています。
CDKのデプロイではどのような権限を使うべきか
<ざっくり要約>
権限付与の方法は一般的に、許可リスト方式と拒否リスト方式の2種類ある。
CDK/CloudFormationにおいてはロールバックもあり、許可リストでは最小権限を網羅するのは難しい。
AWS Config, AWS CloudTrail, AWS Security Hub, AWS CloudFormation Hooksなどによってガードレール、コンプライアンスルール、監査、モニタリングを実施した上で、それらを変更する以外の全ての権限を付与する拒否リスト方式の方が推奨される。コンプライアンスに適合する範囲で開発者に自由を与えることができる。
<感想>
IaCの経験がある方であれば、許可リスト方式での権限付与は厳しいことはよく分かると思います。特に CDK/CloudFormation におけるロールバックは Terraform にはない特徴の一つなので、権限付与に限らず、CDKを使うときはそれを意識するのが良いでしょう。
権限昇格することなくIAMロールの作成を許可する
<ざっくり要約>
CDKでリソースをデプロイするときは、デフォルトで最小権限になるように必要なIAMロールを自動作成する。手動でロールを作らずに、このデフォルトの挙動に任せるのが推奨である。
アプリ開発者にロールを作成させない運用ルールがある場合、その目的は主に開発者の権限昇格を防ぐことである。権限昇格を防ぐ代替手段としてPermissions boundary(アクセス許可境界)とSCP(サービスコントロールポリシー)がある。開発者のロールにPermissions boundaryをアタッチすることで、開発者自身の権限と、開発者のロールが作成するロールの権限両方を制限できる。
<感想>
IAMロールやポリシーはIaCで作る他のリソースを参照するため、手動で管理せずにIaCの開発サイクルに含めた方が管理しやすいのは納得できる説明です。
CDKでインフラをデプロイすると、いつの間にかたくさんのIAMロールが作られていて把握しきれないと感じていましたが、そもそも人間が把握できる程度のシンプルさでは最小権限は実現できていないのだろうと説明を読んで思いました。
CDKデプロイで使われる権限の管理
<ざっくり要約>
CDKはCloudFormationを使って変更をデプロイする。
デプロイを開始するアクター(ユーザーか自動化システム)はIAMアイデンティティ(ユーザーかロール)を持ち、それとは別にデプロイを実行する CloudFormation にもロールが与えられる。
スタックデプロイ時にCDKが使用するロールとアセットコンテナ(後述)は、Stack Synthesizer が決定する。
<感想>
cdk deploy
を実行する時の権限と、実際に CloudFormation がリソースをデプロイする時の権限は分けられることが書いてあります。これも Terraform と比較した時の CDK の特徴ですね。意識的に2つの権限を区別して考えるとこの後の説明が理解しやすいと思います。
Stack Synthesizer については DefaultStackSynthesizer
と CliCredentialsStackSynthesizer
の2種類の説明がこの後に続きます。
DefaultStackSynthesizer
デプロイのフロー
<ざっくり要約>
DefaultStackSynthesizer
は CDK v2 のデフォルトのStack Synthesizerであり、基本的にはこれを使うことになる。
CDKによるデプロイは次のフローの通りである
- CDKデプロイを開始するアクターのAWS権限を認可する
- CDKデプロイを開始するアクターは「CDKデプロイロール」を引き受け(AssumeRole)、「CDKデプロイロール」は「CFN実行ロール」をCloudFormationに渡す(PassRole)。
- 「CFN実行ロール」はCloudFormationサービスロールで、スタックをデプロイする時にCloudFormationに利用される
- これ以降、デプロイに使われるのはデプロイを開始したアクターの権限ではなく、「CFN実行ロール」の権限になる
- 必要に応じてCDKデプロイを開始したアクターは「file-publishing-role」「image-publishing-role」「lookup-role」をそれぞれ引き受ける(AssumeRole)
(図は https://github.com/aws/aws-cdk/wiki/Security-And-Safety-Dev-Guide より)
<感想>
「CDKデプロイを開始するアクターの権限」「CDKデプロイロール」「CFN実行ロール」「file-publishing-role」「image-publishing-role」「lookup-role」と権限が6種類登場しています。
「CDKデプロイを開始するアクターの権限」はCDKを利用する前に手動で作成するもので、「CDKデプロイロール」「file-publishing-role」「image-publishing-role」「lookup-role」の4つをAssumeRoleできれば良いです。すなわち、cdk deploy
を実行する時の権限は AdministratorAccess である必要はありません。
「CDKデプロイロール」「CFN実行ロール」「file-publishing-role」「image-publishing-role」「lookup-role」の5つは cdk bootstrap
するときに自動で作成されるもので、手で管理する必要はありません。bootstrapの説明はこの後続きます。
Bootstrap
<ざっくり要約>
DefaultStackSynthesizer
を使うためにはまずAWS環境を bootstrap する必要がある。bootstrap は各リージョンに一度だけ必要な操作で、Synthesizerに必要なIAMロール群とアセットコンテナ(S3バケットとECRリポジトリ)が作成される。
cdk boostrap
することでCloudFormationスタックがデプロイされる。デフォルトのテンプレートを使うこともできるし、カスタマイズすることもできる。
コンプライアンス管理をする場合、bootstrap時のテンプレートをカスタマイズする必要がある。例えばカスタマイズによって「CFN実行ロール」の権限を制限できる(ポリシーの変更、Permission boundaryの追加などによって権限昇格を防ぐ)。デフォルトでは「CFN実行ロール」はAdministratorAccessとなる。
また「CDKデプロイロール」「file-publishing-role」「image-publishing-role」を誰が AssumeRole できるのかを制限することもできる。デフォルトでは同じAWSアカウントの全てのアイデンティティが AssumeRole できる。
CDKがbootstrapしたロールを引き受けるためには、以下のポリシーステートメントをアイデンティティに追加する(=CDKデプロイを開始するアクターは以下の権限を持てば良い)
{ "Version": "2012-10-17", "Statement": [{ "Sid": "AssumeCDKRoles", "Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "*", "Condition": { "ForAnyValue:StringEquals": { "iam:ResourceTag/aws-cdk:bootstrap-role": [ "image-publishing", "file-publishing", "deploy", "lookup" ] } } }] }
cdk bootstrap
の実行はAdministratorAccessの権限を使うことを推奨する。bootstrapスタックの内容は変わりうるので、将来的に必要な権限は予測できない。またbootstrapによって任意の権限を持った新しいロールが作られるので、bootstrapを実行する権限を制限する実メリットがない。
<感想>
cdk bootstrap
は CDKToolkit
という名前のCloudFormationスタックを作る程度の認識でしたが、実際に何をやっているのかの説明がされていました。デフォルト設定で作成された CDKToolkit
スタックを実際に見てみると、確かに説明されていた通りのリソースが作られています。
- ECRリポジトリ「ContainerAssetsRepository」
- S3バケット「StagingBucket」
- CDKデプロイロール「DeploymentActionRole」
- CloudFormation、S3、KMSの権限や、CFN実行ロールをPassRoleする権限が付いている
aws-cdk:bootstrap-role
:deploy
リソースタグが付いている
- CFN実行ロール「CloudFormationExecutionRole」
- AdministratorAccess ポリシーが付いている
- リソースタグは付いていない
- file-publishing-role「FilePublishingRole」
- S3とKMSへの権限が付いている
aws-cdk:bootstrap-role
:file-publishing
リソースタグが付いている
- image-publishing-role「ImagePublishingRole」
- ECRへの権限が付いている
aws-cdk:bootstrap-role
:image-publishing
リソースタグが付いている
- lookup-role「LookupRole」
- AWS管理の ReadOnlyAccess ポリシーが付いている
aws-cdk:bootstrap-role
:lookup
リソースタグが付いている
注目すべきは、紹介されているポリシーステートメントではリソースタグで「CDKデプロイロール」「file-publishing-role」「image-publishing-role」「lookup-role」のAssumeRoleを許可していて、AdministratorAccess を持つ「CFN実行ロール」をAssumeRoleできないようになっている点です。つまりこれを使うことで、「CDKデプロイを開始するアクター」はAdministratorAccess権限をAssumeRoleできません。
(しかし厳密に考えると、「CDKデプロイロール」は CloudFormationスタックを作ることができて、「CFN実行ロール」をPassRoleできるため、CloudFormationに強権限のIAMロールを作らせて然るべきリソースタグを付けることで、「CDKデプロイを開始するアクター」から強権限のロールに最終的にAssumeRoleできるように思います。AssumeRoleを許可する対象リソースはリソースタグではなくロール名で指定した方がより安全でしょう)
bootstrapで作成されるリソースは将来的に変わる可能性もあるとのことですが、「CDKデプロイを開始するアクター」がAssumeすべきロールの種類が増えていかないかは気になるところです。
CliCredentialsStackSynthesizer
<ざっくり要約>
DefaultStackSynthesizer
の代わりに CliCredentialsStackSynthesizer
を使うことができる。IAMロールをAssumeするのではなく、「CDKデプロイを開始するアクター」の権限を利用する。CloudFormationコールは直接実行され、サービスロールは渡されない。つまり「CDKデプロイを開始するアクター」は bootstrap が本来作成するロールの全ての権限を持つ必要がある。なお --role-arn
パラメータを利用することで「CFN実行ロール」を別のロールとすることはできる。
<感想>
CliCredentialsStackSynthesizer
を使いたいケースがどのような場合があるのか気になります。「CDKデプロイを開始するアクター」(ローカルやデプロイパイプラインに保存するアクセスキー)が強い権限を持つ必要があるので、基本的には DefaultStackSynthesizer
の方を使うのが良いのではないでしょうか。
CDK内のIAMアイデンティティとポリシーを取り扱う
<ざっくり要約>
CDKにIAMロールとポリシーの作成を任せるのが推奨。もしそれを許可できない場合、事前に人の手で作成し、CDKアプリから参照するようにする。
<感想>
CDKデプロイ自体の権限についての説明は終わり、ここからはなじみやすいインフラリソースの権限についてです。
CDKにIAMロールとポリシーの管理を任せる
<ざっくり要約>
CDKに権限の生成を任せる場合、 grant
メソッドを活用するのが良い。例えば暗号化されたS3バケットからの読み取りをLambda関数に許可する場合、bucket.grantRead(handler)
のように書ける。
import { aws_s3 as s3, aws_lambda as lambda, aws_kms as kms, Stack, } from 'aws-cdk-lib'; const stack = new Stack(app, 'LambdaStack'); const key = new kms.Key(stack, 'BucketKey'); const bucket = new s3.Bucket(stack, 'Bucket', { encryptionKey: key, }); const handler = new lambda.Function(stack, 'Handler', { runtime: lambda.Runtime.NODEJS_16_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), }); bucket.grantRead(handler);
handler.addToRolePolicy()
でLambda関数に直接権限を与えることもできるが、grant
メソッドが使える場合はそれが推奨される。
<感想>
CDKには明示的に権限を付与しなくても、(なるべく)最小権限を自動で付与される方法が多く用意されています。例のS3バケットの grantRead
では、以下の権限がLambda関数の実行ロールに付与されます。
- 対象バケットへの
s3:GetObject*
- 対象バケットへの
s3:GetBucket*
- 対象バケットへの
s3:List*
- 対象バケットが利用するKMSキーへの
kms:Decrypt
- 対象バケットが利用するKMSキーへの
kms:DescribeKey
もしLambda関数が GetObject
しか実行しない場合、GetBucket*
や List*
の権限も付与されているので厳密には最小権限ではありませんが、読み取りしか許可されていない意味では気になる状況は少ないでしょう。
grand
メソッドの名前だけでは具体的に付与される権限が分からなかったりするので、メソッドの説明などで確認すると良いでしょう。例えば現状、DynamoDBテーブルのコンストラクトには grantReadWriteData
メソッドが用意されており、実際に付与される権限がメソッドの説明で分かります。
アイテムをPutするだけで、Deleteする権限を与えたくない場合もあるかと思いますので、そのような時は addToRolePolicy
などを使うことになります。
事前に手動で作成したロールを利用する
<ざっくり要約>
CDKでIAMロールとポリシーの作成が許可されていない場合、事前に手動で作成し、Role.fromRoleName()
で参照する。
あるいは Role.customizeRoles()
を利用して、CDKアプリに必要となる全てのロールとポリシーを実際に作成せずに、レポートとして書き出してIAMロールを管理する担当者に連携することができる。
const stack = new Stack(app, 'LambdaStack'); // ロールのsynthをしないようにこれを追加する iam.Role.customizeRoles(stack); // 以下のコードは同じ const key = new kms.Key(stack, 'BucketKey'); const bucket = new s3.Bucket(stack, 'Bucket', { encryptionKey: key, }); const handler = new lambda.Function(stack, 'Handler', { runtime: lambda.Runtime.NODEJS_16_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), }); // 権限が必要であることをCDKに教えるため、通常通り grantRead() などは必要 bucket.grantRead(handler);
人の手によってロールが作成されたら、そのロール名を customizeRoles
に追加することでCDKで利用できるようになる。
const stack = new Stack(app, 'LambdaStack'); iam.Role.customizeRoles(stack, { usePrecreatedRoles: { 'LambdaStack/Handler/ServiceRole': 'lambda-service-role', }, });
<感想>
customizeRolesはCDK v2.51.0でリリースされた機能です。IAM権限を開発者が自由に作成できないような環境では役に立ちそうです。
Permissions boundary と SCP
<ざっくり要約>
セキュリティガードレールのためにPermissions boundaryを使うことができる。
権限昇格を防ぐために、同じPermissions boundaryを含まないようなユーザーやロールを作ることを禁止できる。
cdk bootstrap
で --custom-permissions-boundary
フラグを追加することで、「CFN実行ロール」にPermissions boundaryをアタッチできる。
CDKアプリ内で作るロールにPermissions boundaryをアタッチする場合は以下のとおりになる。
const stack = new Stack(app, 'MyStack'); new iam.Role(stack, 'Role', { assumeRolePolicy: new iam.ServicePrincipal('lambda.amazonaws.com', permissionsBoundary: iam.ManagedPolicy.fromManagedPolicyName(`cdk-${stack.synthesizer.bootstrapQualifier}-permissions-boundary-${stack.account}-${stack.region}`); });
cdk.json
にてグローバルで設定もできる。
{ "context": { "@aws-cdk/core:permissionsBoundary": { "name": "cdk-permissions-boundary" } } }
App や Stage レベルで設定もできる。
<感想>
Permissions boundary をCDKで利用する方法について詳細に書かれていました。
--custom-permissions-boundary
フラグは CDK v2.54.0 に追加された機能です。
ざっくり要約をさらに要約
- CDKのデプロイに与える権限は許可リストではなく、拒否リストが推奨である
- IAMロールは手動で作るのではなく、CDKの自動作成に任せるのが推奨である
- 開発者の権限昇格を防ぎたい場合、Permissions boundary(アクセス許可境界)とSCP(サービスコントロールポリシー)を利用する
- bootstrap 時のCloudFormationテンプレートをカスタマイズする
cdk bootstrap
はAdministratorAccessの権限で行う- 基本的に
cdk deploy
にAdministratorAccessは必要なく、bootstrapで作成された4種類のIAMロールをAssumeRoleできれば良い - CDKに権限の生成を任せる場合、なるべく
grant
メソッドを利用する Role.customizeRoles()
を利用することで、CDKデプロイせずに必要となる権限一覧をレポート出力できる
お読みいただいてありがとうございました。
私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。
セキュリティエンジニア(セキュリティ設計)執筆:@kou.kinyo、レビュー:@wakamoto.ryosuke (Shodoで執筆されました)