【AWS CDK】CDK Bootstrap時のアクセス許可境界設定についての備忘録

## はじめに
CDKを用いたAWSリソースの構築でも始めようかなと思ったときに、「いい感じで最低限のアクセス制限だけしたい」ということで試行錯誤した結果をまとめたものです。

どうやら、CDK環境のBootstrap時にアクセス許可境界の設定をすれば良いと分かったので、CDKが何かとかBootstrapとは、というのは置いといて、自分が行った設定の仕方を備忘録として書きます。

:::note warn
ベストプラクティスというわけではないので、あしからず。
:::

### 動作環境
- AWS CDK: v2.1004.0
- Node.js: v23.10.0

## IAMのアクセス許可境界
AWSアカウント内で「管理者権限」と「開発者権限」を分け、普段使用する開発者用のエンティティ（IAMユーザー/IAMロール）では権限を制限させたくなります。AWS IAMには、それを叶える機能として、<strong>アクセス許可境界（Permissions Boundary）</strong>があります。（知らなかった。）

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_policies_boundaries.html

簡単に説明すると、IAMユーザーやIAMロールへアタッチする**制限事項**であり、それによって権限の昇格を阻止します。許可ポリシーの方でアクセス許可されたアクションでも、許可境界のポリシーで拒否されていたら、そのアクションは実行できません。

アクセス許可境界を設定するには、そのためのポリシーを作成するのがいいらしいです。[こちらのAWSブログの記事](https://aws.amazon.com/jp/blogs/news/secure-cdk-deployments-with-iam-permission-boundaries/)の紹介を参考にポリシーを作ります。

```json:permissions-boundary-policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ExplicitAllowAll",
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        },
        {
            "Sid": "DenyAccessIfRequiredPermBoundaryIsNotBeingApplied",
            "Effect": "Deny",
            "Action": [
                "iam:PutRolePermissionsBoundary",
                "iam:PutUserPermissionsBoundary",
                "iam:CreateRole",
                "iam:CreateUser"
            ],
            "Resource": "*",
            "Condition": {
                "ArnNotEquals": {
                    "iam:PermissionsBoundary": "arn:aws:iam::{AccountId}:policy/permissions-boundary-policy"
                }
            }
        },
        {
            "Sid": "DenyUpdateIfRequiredPermBoundaryIsNotBeingApplied",
            "Effect": "Deny",
            "Action": [
                "iam:DetachRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:PutUserPolicy",
                "iam:DeleteUserPolicy",
                "iam:AttachUserPolicy",
                "iam:AttachRolePolicy",
                "iam:PutRolePolicy",
                "iam:DetachUserPolicy"
            ],
            "Resource": [
                "arn:aws:iam::{AccountId}:role/*",
                "arn:aws:iam::{AccountId}:user/*"
            ],
            "Condition": {
                "ArnNotEquals": {
                    "iam:PermissionsBoundary": "arn:aws:iam::{AccountId}:policy/permissions-boundary-policy"
                }
            }
        },
        {
            "Sid": "DenyPermBoundaryIAMPolicyAlteration",
            "Effect": "Deny",
            "Action": [
                "iam:DeletePolicy",
                "iam:CreatePolicyVersion",
                "iam:DeletePolicyVersion",
                "iam:SetDefaultPolicyVersion"
            ],
            "Resource": "arn:aws:iam::{AccountId}:policy/permissions-boundary-policy"
        },
        {
            "Sid": "DenyRemovalOfPermBoundaryFromAnyUserOrRole",
            "Effect": "Deny",
            "Action": [
                "iam:DeleteUserPermissionsBoundary",
                "iam:DeleteRolePermissionsBoundary"
            ],
            "Resource": "*"
        },
        {
            "Sid": "CustomizedDenyActions1",
            "Effect": "Deny",
            "Action": [
                "iam:AttachGroupPolicy",
                "iam:AttachUserPolicy",
                "iam:DetachGroupPolicy",
                "iam:DetachUserPolicy"
            ],
            "Resource": [
                "arn:aws:iam::{AccountId}:user/*",
                "arn:aws:iam::{AccountId}:group/*"
            ],
            "Condition": {
                "ArnEquals": {
                    "iam:PolicyARN": "arn:aws:iam::aws:policy/AdministratorAccess"
                }
            }
        }
    ]
}
```

上記のポリシーの`"CustomizedDenyActions1"`のように、開発者に実行させたくないアクションがあればここに追加していく、という運用方法です。これが許可境界として設定されたエンティティは、このポリシーを編集できません。

<details><summary>permissions-boundary-policyの補足</summary>

`"DenyUpdateIfRequiredPermBoundaryIsNotBeingApplied"`は、参考にしたブログ内には無いステートメントです。
これは、以下のような抜け道が考えられると思ったので、ポリシーに追加しました。
1. 許可境界が設定されていない既存のロールに、最高権限のポリシーをアタッチする
2. そのロールを何かのエンティティに付与する

上記のステートメントは、この内の 1. の操作を防ぎます。

---
</details>

## アクセス許可境界をCDKデプロイに適用
次は、先程作成した許可境界を、CDKのデプロイの際に適用されるようにしたいと思います。

:::note warn
以降はcdkコマンドを実行する開発者用エンティティに、`permissions-boundary-policy`の許可境界が設定されている前提です。
:::

### Bootstrap時に指定
CDKについて調べると、AWSリソースを作成するデプロイに使用されるロールは、CDKの**Bootstrap**の実行時に生成されるようです。したがって、[公式のドキュメント](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/customize-permissions-boundaries.html#customize-permissions-boundaries-how)にもある通り、Bootstrap時に適用したいアクセス許可境界を指定します。

しかし、開発者がこのまま以下コマンドでBootstrapすると、エラーで失敗します。
```
cdk bootstrap --custom-permissions-boundary permissions-boundary-policy
```
なぜなら、Bootstrapで生成されるロールに、**今回のアクセス許可境界が設定されない**ロールがあり、`iam:CreateRole`を実行できないからです。

色々回避策は考えられますが、自分は「自動生成されるロールすべてにアクセス許可境界を設定する」方法で行きたいと思います。

### 自動生成されるロールすべてにアクセス許可境界を設定
この設定をするためには、Bootstrapに用いられるテンプレートをカスタマイズする必要があるみたいです。[^1]

[^1]: https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/bootstrapping-customizing.html#bootstrapping-customizing-template

まず、デフォルトで用いられるテンプレートをyamlファイルに出力します。
```
cdk bootstrap --show-template > bootstrap-template.yaml
```
:::note
このyamlファイルの中身は[cfnテンプレート](https://aws.amazon.com/jp/cloudformation/resources/templates/)
:::

そして、そのyamlファイルを編集して、**生成されるロールに自前のポリシーをアクセス許可境界として設定します**。以下はその手順についてです。

- Bootstrap時には以下のロールを作成します[^2]。つまり、yamlファイルの`Resources`に記載されています
    - `CloudFormationExecutionRole`
    - `DeploymentActionRole`
    - `FilePublishingRole`
    - `ImagePublishingRole`
    - `LookupRole`
    <br>

- そのうちの`CloudFormationExecutionRole`だけは、<strong>`--custom-permissions-boundary`オプションで指定されたポリシーを許可境界に設定する</strong>ように実装されています
    ```yaml
    PermissionsBoundary:
        Fn::If:
          - PermissionsBoundarySet
          - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}
          - Ref: CdkBoostrapPermissionsBoundaryPolicy
    ```
    <br>

- ということなので、他の4つのロールに↑の実装をコピーします

以上で、テンプレートのカスタマイズは完了です。

[^2]: https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/bootstrapping-env.html#bootstrapping-env-default

:::note
編集後のyamlファイルの全コードは、本記事の[おわり](#コード)に貼付しています。
:::

### CDK Bootstrapを再度実行
必要な設定が終わったので、以下コマンドで再度Bootstrapを実行します。
※ `--qualifier`オプションについては　→　[参考記事](https://engineering.mobalab.net/2023/09/12/rethinking-of-aws-cdk-permissions/#[%E6%89%8B%E8%BB%BD]%20Qualifier%20%E3%82%92%E5%A4%89%E3%81%88%E3%82%8B)
```
cdk bootstrap \
    --template bootstrap-template.yaml \
    --custom-permissions-boundary permissions-boundary-policy \
    --qualifier hoge
```
```:結果
✅  Environment aws://{AccountId}/{Region} bootstrapped.
```

Bootstrapが成功しました。これで、CDKで利用されるロールにアクセス許可境界が設定されています。

### CDK デプロイのための設定
Bootstrapで、アクセス許可境界やQualifierをカスタマイズした場合は、`cdk.json`に以下を追記します。
```diff_json:cdk.json
{
    ...
    "context": {
        ...
+       "@aws-cdk/core:permissionsBoundary": {
+         "name": "permissions-boundary-policy"
+       },
+       "@aws-cdk/core:bootstrapQualifier": "hoge"
    }
}
```
これにより、CDKで構築したIAMエンティティには自動で、`permissions-boundary-policy`が許可境界として適用されるようになります。

### Bootstrapのやり直し方
Bootstrapをやり直したい場合、以下の手順で作成されたリソースを完全に削除します。
1. CloudFormationのスタック削除
    デフォルトならスタック名は、`CDKToolkit`
1. S3のバケット削除
    デフォルトならバケット名は、`cdk-{Qualifier}-assets-{AccountId}-{Region}`

:::note warn
CloudFormationのスタックを消しても、S3のバケットだけは残り続ける点に注意。
:::

## 実験してみた
今回の`permissions-boundary-policy`では、マネージドポリシーの`AdministratorAccess`のアタッチを禁止にしているので、これをCDKのデプロイで試してみます。
```typescript:role-stack.ts
const user = new iam.User(scope, 'SampleAdministrator', {
    userName: 'SampleAdministrator',
    managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')
    ]
});
```
```
cdk deploy SampleIamStack
```
すると、期待通り権限エラーが発生しデプロイは失敗しました。
```:結果
User: arn:aws:sts:: （略） /AWSCloudFormation is not authorized to perform: iam:AttachRolePolicy on resource: user SampleAdministrator with an explicit deny in a permissions boundary
```

## おわり
以上で、CDKによるデプロイに対するアクセス制限を設定できました。

必要十分であるという確証はないですが、最低限の制限はできたかと思っています。アクセス許可境界を理解するために2日間頭を悩ませた甲斐がありました。

### コード
<details><summary>編集後のbootstrap-template.yamlの全コード</summary>

```yaml
Description: This stack includes resources needed to deploy AWS CDK apps into this environment
Parameters:
  TrustedAccounts:
    Description: List of AWS accounts that are trusted to publish assets and deploy stacks to this environment
    Default: ""
    Type: CommaDelimitedList
  TrustedAccountsForLookup:
    Description: List of AWS accounts that are trusted to look up values in this environment
    Default: ""
    Type: CommaDelimitedList
  CloudFormationExecutionPolicies:
    Description: List of the ManagedPolicy ARN(s) to attach to the CloudFormation deployment role
    Default: ""
    Type: CommaDelimitedList
  FileAssetsBucketName:
    Description: The name of the S3 bucket used for file assets
    Default: ""
    Type: String
  FileAssetsBucketKmsKeyId:
    Description: Empty to create a new key (default), 'AWS_MANAGED_KEY' to use a managed S3 key, or the ID/ARN of an existing key.
    Default: ""
    Type: String
  ContainerAssetsRepositoryName:
    Description: A user-provided custom name to use for the container assets ECR repository
    Default: ""
    Type: String
  Qualifier:
    Description: An identifier to distinguish multiple bootstrap stacks in the same environment
    Default: hnb659fds
    Type: String
    AllowedPattern: "[A-Za-z0-9_-]{1,10}"
    ConstraintDescription: Qualifier must be an alphanumeric identifier of at most 10 characters
  PublicAccessBlockConfiguration:
    Description: Whether or not to enable S3 Staging Bucket Public Access Block Configuration
    Default: "true"
    Type: String
    AllowedValues:
      - "true"
      - "false"
  InputPermissionsBoundary:
    Description: Whether or not to use either the CDK supplied or custom permissions boundary
    Default: ""
    Type: String
  UseExamplePermissionsBoundary:
    Default: "false"
    AllowedValues:
      - "true"
      - "false"
    Type: String
  BootstrapVariant:
    Type: String
    Default: "AWS CDK: Default Resources"
    Description: Describe the provenance of the resources in this bootstrap stack. Change this when you customize the template. To prevent accidents, the CDK CLI will not overwrite bootstrap stacks with a different variant.
Conditions:
  HasTrustedAccounts:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Fn::Join:
              - ""
              - Ref: TrustedAccounts
  HasTrustedAccountsForLookup:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Fn::Join:
              - ""
              - Ref: TrustedAccountsForLookup
  HasCloudFormationExecutionPolicies:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Fn::Join:
              - ""
              - Ref: CloudFormationExecutionPolicies
  HasCustomFileAssetsBucketName:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Ref: FileAssetsBucketName
  CreateNewKey:
    Fn::Equals:
      - ""
      - Ref: FileAssetsBucketKmsKeyId
  UseAwsManagedKey:
    Fn::Equals:
      - AWS_MANAGED_KEY
      - Ref: FileAssetsBucketKmsKeyId
  ShouldCreatePermissionsBoundary:
    Fn::Equals:
      - "true"
      - Ref: UseExamplePermissionsBoundary
  PermissionsBoundarySet:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Ref: InputPermissionsBoundary
  HasCustomContainerAssetsRepositoryName:
    Fn::Not:
      - Fn::Equals:
          - ""
          - Ref: ContainerAssetsRepositoryName
  UsePublicAccessBlockConfiguration:
    Fn::Equals:
      - "true"
      - Ref: PublicAccessBlockConfiguration
Resources:
  FileAssetsBucketEncryptionKey:
    Type: AWS::KMS::Key
    Properties:
      KeyPolicy:
        Statement:
          - Action:
              - kms:Create*
              - kms:Describe*
              - kms:Enable*
              - kms:List*
              - kms:Put*
              - kms:Update*
              - kms:Revoke*
              - kms:Disable*
              - kms:Get*
              - kms:Delete*
              - kms:ScheduleKeyDeletion
              - kms:CancelKeyDeletion
              - kms:GenerateDataKey
              - kms:TagResource
              - kms:UntagResource
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
            Resource: "*"
          - Action:
              - kms:Decrypt
              - kms:DescribeKey
              - kms:Encrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
            Effect: Allow
            Principal:
              AWS: "*"
            Resource: "*"
            Condition:
              StringEquals:
                kms:CallerAccount:
                  Ref: AWS::AccountId
                kms:ViaService:
                  - Fn::Sub: s3.${AWS::Region}.amazonaws.com
          - Action:
              - kms:Decrypt
              - kms:DescribeKey
              - kms:Encrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
            Effect: Allow
            Principal:
              AWS:
                Fn::Sub: ${FilePublishingRole.Arn}
            Resource: "*"
    Condition: CreateNewKey
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
  FileAssetsBucketEncryptionKeyAlias:
    Condition: CreateNewKey
    Type: AWS::KMS::Alias
    Properties:
      AliasName:
        Fn::Sub: alias/cdk-${Qualifier}-assets-key
      TargetKeyId:
        Ref: FileAssetsBucketEncryptionKey
  StagingBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName:
        Fn::If:
          - HasCustomFileAssetsBucketName
          - Fn::Sub: ${FileAssetsBucketName}
          - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region}
      AccessControl: Private
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
              KMSMasterKeyID:
                Fn::If:
                  - CreateNewKey
                  - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn}
                  - Fn::If:
                      - UseAwsManagedKey
                      - Ref: AWS::NoValue
                      - Fn::Sub: ${FileAssetsBucketKmsKeyId}
      PublicAccessBlockConfiguration:
        Fn::If:
          - UsePublicAccessBlockConfiguration
          - BlockPublicAcls: true
            BlockPublicPolicy: true
            IgnorePublicAcls: true
            RestrictPublicBuckets: true
          - Ref: AWS::NoValue
      VersioningConfiguration:
        Status: Enabled
      LifecycleConfiguration:
        Rules:
          - Id: CleanupOldVersions
            Status: Enabled
            NoncurrentVersionExpiration:
              NoncurrentDays: 30
          - Id: AbortIncompleteMultipartUploads
            Status: Enabled
            AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 1
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
  StagingBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket:
        Ref: StagingBucket
      PolicyDocument:
        Id: AccessControl
        Version: "2012-10-17"
        Statement:
          - Sid: AllowSSLRequestsOnly
            Action: s3:*
            Effect: Deny
            Resource:
              - Fn::Sub: ${StagingBucket.Arn}
              - Fn::Sub: ${StagingBucket.Arn}/*
            Condition:
              Bool:
                aws:SecureTransport: "false"
            Principal: "*"
  ContainerAssetsRepository:
    Type: AWS::ECR::Repository
    Properties:
      ImageTagMutability: IMMUTABLE
      LifecyclePolicy:
        LifecyclePolicyText: |
          {
            "rules": [
              {
                "rulePriority": 1,
                "description": "Untagged images should not exist, but expire any older than one year",
                "selection": {
                  "tagStatus": "untagged",
                  "countType": "sinceImagePushed",
                  "countUnit": "days",
                  "countNumber": 365
                },
                "action": { "type": "expire" }
              }
            ]
          }
      RepositoryName:
        Fn::If:
          - HasCustomContainerAssetsRepositoryName
          - Fn::Sub: ${ContainerAssetsRepositoryName}
          - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region}
      RepositoryPolicyText:
        Version: "2012-10-17"
        Statement:
          - Sid: LambdaECRImageRetrievalPolicy
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action:
              - ecr:BatchGetImage
              - ecr:GetDownloadUrlForLayer
            Condition:
              StringLike:
                aws:sourceArn:
                  Fn::Sub: arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*
          - Sid: EmrServerlessImageRetrievalPolicy
            Effect: Allow
            Principal:
              Service: emr-serverless.amazonaws.com
            Action:
              - ecr:BatchGetImage
              - ecr:GetDownloadUrlForLayer
              - ecr:DescribeImages
            Condition:
              StringLike:
                aws:sourceArn:
                  Fn::Sub: arn:${AWS::Partition}:emr-serverless:${AWS::Region}:${AWS::AccountId}:/applications/*
  FilePublishingRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:TagSession
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Fn::If:
              - HasTrustedAccounts
              - Action: sts:AssumeRole
                Effect: Allow
                Principal:
                  AWS:
                    Ref: TrustedAccounts
              - Ref: AWS::NoValue
      RoleName:
        Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region}
      PermissionsBoundary:
        Fn::If:
          - PermissionsBoundarySet
          - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}
          - Ref: CdkBoostrapPermissionsBoundaryPolicy
      Tags:
        - Key: aws-cdk:bootstrap-role
          Value: file-publishing
  ImagePublishingRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:TagSession
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Fn::If:
              - HasTrustedAccounts
              - Action: sts:AssumeRole
                Effect: Allow
                Principal:
                  AWS:
                    Ref: TrustedAccounts
              - Ref: AWS::NoValue
      RoleName:
        Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region}
      PermissionsBoundary:
        Fn::If:
          - PermissionsBoundarySet
          - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}
          - Ref: CdkBoostrapPermissionsBoundaryPolicy
      Tags:
        - Key: aws-cdk:bootstrap-role
          Value: image-publishing
  LookupRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:TagSession
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Fn::If:
              - HasTrustedAccountsForLookup
              - Action: sts:AssumeRole
                Effect: Allow
                Principal:
                  AWS:
                    Ref: TrustedAccountsForLookup
              - Ref: AWS::NoValue
          - Fn::If:
              - HasTrustedAccounts
              - Action: sts:AssumeRole
                Effect: Allow
                Principal:
                  AWS:
                    Ref: TrustedAccounts
              - Ref: AWS::NoValue
      RoleName:
        Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region}
      ManagedPolicyArns:
        - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess
      Policies:
        - PolicyDocument:
            Statement:
              - Sid: DontReadSecrets
                Effect: Deny
                Action:
                  - kms:Decrypt
                Resource: "*"
            Version: "2012-10-17"
          PolicyName: LookupRolePolicy
      PermissionsBoundary:
        Fn::If:
          - PermissionsBoundarySet
          - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}
          - Ref: CdkBoostrapPermissionsBoundaryPolicy
      Tags:
        - Key: aws-cdk:bootstrap-role
          Value: lookup
  FilePublishingRoleDefaultPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - s3:GetObject*
              - s3:GetBucket*
              - s3:GetEncryptionConfiguration
              - s3:List*
              - s3:DeleteObject*
              - s3:PutObject*
              - s3:Abort*
            Resource:
              - Fn::Sub: ${StagingBucket.Arn}
              - Fn::Sub: ${StagingBucket.Arn}/*
            Condition:
              StringEquals:
                aws:ResourceAccount:
                  - Fn::Sub: ${AWS::AccountId}
            Effect: Allow
          - Action:
              - kms:Decrypt
              - kms:DescribeKey
              - kms:Encrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
            Effect: Allow
            Resource:
              Fn::If:
                - CreateNewKey
                - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn}
                - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId}
        Version: "2012-10-17"
      Roles:
        - Ref: FilePublishingRole
      PolicyName:
        Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region}
  ImagePublishingRoleDefaultPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action:
              - ecr:PutImage
              - ecr:InitiateLayerUpload
              - ecr:UploadLayerPart
              - ecr:CompleteLayerUpload
              - ecr:BatchCheckLayerAvailability
              - ecr:DescribeRepositories
              - ecr:DescribeImages
              - ecr:BatchGetImage
              - ecr:GetDownloadUrlForLayer
            Resource:
              Fn::Sub: ${ContainerAssetsRepository.Arn}
            Effect: Allow
          - Action:
              - ecr:GetAuthorizationToken
            Resource: "*"
            Effect: Allow
        Version: "2012-10-17"
      Roles:
        - Ref: ImagePublishingRole
      PolicyName:
        Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region}
  DeploymentActionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:TagSession
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Ref: AWS::AccountId
          - Fn::If:
              - HasTrustedAccounts
              - Action: sts:AssumeRole
                Effect: Allow
                Principal:
                  AWS:
                    Ref: TrustedAccounts
              - Ref: AWS::NoValue
      Policies:
        - PolicyDocument:
            Statement:
              - Sid: CloudFormationPermissions
                Effect: Allow
                Action:
                  - cloudformation:CreateChangeSet
                  - cloudformation:DeleteChangeSet
                  - cloudformation:DescribeChangeSet
                  - cloudformation:DescribeStacks
                  - cloudformation:ExecuteChangeSet
                  - cloudformation:CreateStack
                  - cloudformation:UpdateStack
                  - cloudformation:RollbackStack
                  - cloudformation:ContinueUpdateRollback
                Resource: "*"
              - Sid: PipelineCrossAccountArtifactsBucket
                Effect: Allow
                Action:
                  - s3:GetObject*
                  - s3:GetBucket*
                  - s3:List*
                  - s3:Abort*
                  - s3:DeleteObject*
                  - s3:PutObject*
                Resource: "*"
                Condition:
                  StringNotEquals:
                    s3:ResourceAccount:
                      Ref: AWS::AccountId
              - Sid: PipelineCrossAccountArtifactsKey
                Effect: Allow
                Action:
                  - kms:Decrypt
                  - kms:DescribeKey
                  - kms:Encrypt
                  - kms:ReEncrypt*
                  - kms:GenerateDataKey*
                Resource: "*"
                Condition:
                  StringEquals:
                    kms:ViaService:
                      Fn::Sub: s3.${AWS::Region}.amazonaws.com
              - Action: iam:PassRole
                Resource:
                  Fn::Sub: ${CloudFormationExecutionRole.Arn}
                Effect: Allow
              - Sid: CliPermissions
                Action:
                  - cloudformation:DescribeStackEvents
                  - cloudformation:GetTemplate
                  - cloudformation:DeleteStack
                  - cloudformation:UpdateTerminationProtection
                  - sts:GetCallerIdentity
                  - cloudformation:GetTemplateSummary
                Resource: "*"
                Effect: Allow
              - Sid: CliStagingBucket
                Effect: Allow
                Action:
                  - s3:GetObject*
                  - s3:GetBucket*
                  - s3:List*
                Resource:
                  - Fn::Sub: ${StagingBucket.Arn}
                  - Fn::Sub: ${StagingBucket.Arn}/*
              - Sid: ReadVersion
                Effect: Allow
                Action:
                  - ssm:GetParameter
                  - ssm:GetParameters
                Resource:
                  - Fn::Sub: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion}
            Version: "2012-10-17"
          PolicyName: default
      RoleName:
        Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region}
      PermissionsBoundary:
        Fn::If:
          - PermissionsBoundarySet
          - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}
          - Ref: CdkBoostrapPermissionsBoundaryPolicy
      Tags:
        - Key: aws-cdk:bootstrap-role
          Value: deploy
  CloudFormationExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        Fn::If:
          - HasCloudFormationExecutionPolicies
          - Ref: CloudFormationExecutionPolicies
          - Fn::If:
              - HasTrustedAccounts
              - Ref: AWS::NoValue
              - - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess
      RoleName:
        Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region}
      PermissionsBoundary:
        Fn::If:
          - PermissionsBoundarySet
          - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}
          - Ref: CdkBoostrapPermissionsBoundaryPolicy
  CdkBoostrapPermissionsBoundaryPolicy:
    Condition: ShouldCreatePermissionsBoundary
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Statement:
          - Sid: ExplicitAllowAll
            Action:
              - "*"
            Effect: Allow
            Resource: "*"
          - Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied
            Action:
              - iam:CreateUser
              - iam:CreateRole
              - iam:PutRolePermissionsBoundary
              - iam:PutUserPermissionsBoundary
            Condition:
              StringNotEquals:
                iam:PermissionsBoundary:
                  Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
            Effect: Deny
            Resource: "*"
          - Sid: DenyPermBoundaryIAMPolicyAlteration
            Action:
              - iam:CreatePolicyVersion
              - iam:DeletePolicy
              - iam:DeletePolicyVersion
              - iam:SetDefaultPolicyVersion
            Effect: Deny
            Resource:
              Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
          - Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole
            Action:
              - iam:DeleteUserPermissionsBoundary
              - iam:DeleteRolePermissionsBoundary
            Effect: Deny
            Resource: "*"
        Version: "2012-10-17"
      Description: Bootstrap Permission Boundary
      ManagedPolicyName:
        Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region}
      Path: /
  CdkBootstrapVersion:
    Type: AWS::SSM::Parameter
    Properties:
      Type: String
      Name:
        Fn::Sub: /cdk-bootstrap/${Qualifier}/version
      Value: "27"
Outputs:
  BucketName:
    Description: The name of the S3 bucket owned by the CDK toolkit stack
    Value:
      Fn::Sub: ${StagingBucket}
  BucketDomainName:
    Description: The domain name of the S3 bucket owned by the CDK toolkit stack
    Value:
      Fn::Sub: ${StagingBucket.RegionalDomainName}
  FileAssetKeyArn:
    Description: The ARN of the KMS key used to encrypt the asset bucket (deprecated)
    Value:
      Fn::If:
        - CreateNewKey
        - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn}
        - Fn::Sub: ${FileAssetsBucketKmsKeyId}
    Export:
      Name:
        Fn::Sub: CdkBootstrap-${Qualifier}-FileAssetKeyArn
  ImageRepositoryName:
    Description: The name of the ECR repository which hosts docker image assets
    Value:
      Fn::Sub: ${ContainerAssetsRepository}
  BootstrapVersion:
    Description: The version of the bootstrap resources that are currently mastered in this stack
    Value:
      Fn::GetAtt:
        - CdkBootstrapVersion
        - Value
```
</details>

## 参考リンク
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_policies_boundaries.html

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_controlling.html#access_controlling-policies

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/bootstrapping-env.html#bootstrapping-env-permissions

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/ref-cli-cmd-bootstrap.html

https://aws.amazon.com/jp/blogs/news/secure-cdk-deployments-with-iam-permission-boundaries/

https://zenn.dev/yumemi_inc/articles/cdk-with-permissions-boundary#bootstrapping

https://engineering.mobalab.net/2023/09/12/rethinking-of-aws-cdk-permissions/