【AWS】Cognitoで認証したユーザーごとに専用のCMKを構築する方法 ～CLIで検証してみた～

# はじめに

本記事では、Cognito で認証したユーザーに対して、専用の CMK を構築し、他のユーザーが触れないようにアクセス制御する方法をご紹介します。  

珍しい要件であり、同様の記事がみつからなかったため、備忘として記事化しました。

# 実現したいこと

実現したいことは、以下の通りです。

- Cognito ユーザー[^1]毎に専用の [CMK](https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/concepts.html#customer-cmk) を構築  
  (100 ユーザーいれば、100 の CMK ができるイメージ)
- 各 CMK に複数の Cognito ユーザーがアクセスすることは不可

[^1]: 本記事では[Cognito ユーザープールで認証したユーザー](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html)のことを、Cognito ユーザーと呼んでいます。

以下のイメージ図をご覧ください。

![イメージ図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/0f2deb9a-5855-af64-6a14-11424cd433f2.png)

:::note warn
注意点として、ID プールに紐づけられる認証済みユーザー用の IAM ロールは**1つのみ**です。
上記構成では、全 Cognito ユーザーで IAM ロールを共有するため、IAM ロールで CMK のアクセス先制御を行うのは難しそうです。
:::

# 結論

キーポリシーの Condition 句で、[cognito-identity.amazonaws.com:sub](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/iam-roles.html#trust-policies) を使用することで、上記を実現可能です。以下をご参照ください。

```json: キーポリシー
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Enable IAM policies",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::${ACCOUNT_ID}:root"
      },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "Allow a specific cognito user to use the key",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "kms:Describe*",
        "kms:Get*",
        "kms:List*"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "cognito-identity.amazonaws.com:sub": "${IDENTITY_ID}",
          "aws:userid": "${AUTH_ROLE_ID}:CognitoIdentityCredentials"
        }
      }
    }
  ]
}
```

:::note
[cognito-identity.amazonaws.com:sub](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/iam-roles.html#trust-policies) で IdentityId を指定することにより、特定の Cognito ユーザーのみに CMK アクセスを制限することが可能です。[^2]  
アクセス元の IAM ロールは限定されているので、[aws:userid](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_variables.html#principaltable) では認証済みユーザー用の IAM ロール ID を指定しています。

なお、キーポリシーで KMS 権限を付与しているため、IAM ロールに KMS 権限は不要です。Sid「Enable IAM policies」では[デフォルトのキーポリシー](https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/key-policy-default.html)を使用していますが、必要に応じて権限を絞ってください。[^3]
:::

[^2]: 記事用に Action 句で幅広い権限を付与していますが、実開発では権限を絞っています。

[^3]: [IAM ポリシーが AWS KMS の KMS キーへのアクセスをユーザーまたはロールに許可しないようにするにはどうすればよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/kms-prevent-access/)

# 構築方法

## 前提

事前に、Cognito ユーザープール・ID プール作成済であることを前提とします。  
Amplify でのCognito関連構築方法に関しては、以下をご参照ください。  
```amplify add auth```で簡単に認証機能を実装できます。 

https://aws.amazon.com/jp/getting-started/hands-on/build-react-app-amplify-graphql/

以下、Admin 相当の権限を保有する Cloud9 のターミナルより、AWS CLI で Cognito ユーザと専用の CMK を作成します。[^4] 

[^4]: 各入力値・実行結果は、記事用に仮値へ変換していますので、ご留意ください。

- OS : Amazon Linux 2
- AWS CLI : v2.10.1

## 共通値の設定

まずは、アカウント・リージョン情報や事前作成済みの Cognito に関する値を設定します。  

```Bash
# 共通値
$ ACCOUNT_ID=012345678901
$ REGION=ap-northeast-1
$ USER_POOL_ID=${REGION}_123xyABCD
$ CLIENT_ID=abcdefghijklmnop1234567890
$ IDENTITY_POOL_ID=${REGION}:11111111-1111-1111-1111-111111111111
$ COGNITO_USER_POOL=cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID}
$ AUTH_ROLE_NAME=amplify-react-dev-01234-authRole
```

## Cognito ユーザー作成

次に、テスト用の Cognito ユーザーとして、「test_user1」を作成します。

```Bash
# ユーザー情報
$ USER_NAME=test_user1
$ PASSWORD="Password@1"

# 「test_user1」を作成
$ aws cognito-idp admin-create-user \
  --user-pool-id ${USER_POOL_ID} \
  --username ${USER_NAME} \
  --user-attributes Name=email,Value="test@sample.com" Name=email_verified,Value=true \
  --message-action SUPPRESS > /dev/null
$ aws cognito-idp admin-set-user-password \
  --user-pool-id ${USER_POOL_ID} \
  --username ${USER_NAME} \
  --password ${PASSWORD} \
  --permanent
```

## CMKの作成

専用 CMK 作成の下準備として、test_user1 の IdentityId を取得します。

```Bash
# トークン取得（IdentityId取得に必要）
$ ID_TOKEN=$(aws cognito-idp admin-initiate-auth \
  --user-pool-id ${USER_POOL_ID} \
  --client-id ${CLIENT_ID} \
  --auth-flow ADMIN_NO_SRP_AUTH \
  --auth-parameters "USERNAME=${USER_NAME},PASSWORD=${PASSWORD}" \
  --query "AuthenticationResult.IdToken" \
  --output text)

# IdentityId取得
$ IDENTITY_ID=$(aws cognito-identity get-id \
  --identity-pool-id ${IDENTITY_POOL_ID} \
  --logins "${COGNITO_USER_POOL}=${ID_TOKEN}" \
  --query "IdentityId" \
  --output text) && echo ${IDENTITY_ID}
```

```Bash: (実行結果) IDENTITY_ID
ap-northeast-1:1234abcd-a123-b234-c345-222222222222
```

次に、キーポリシーで許可する IAM ロールを絞るため、Cognito 認証後のロール ID を取得します。

```Bash
# Cognito 認証済みユーザー用の IAM ロール IDを取得
$ AUTH_ROLE_ID=$(aws iam get-role \
  --role-name ${AUTH_ROLE_NAME} \
  --query "Role.RoleId" \
  --output text) && echo ${AUTH_ROLE_ID}
```

```Bash: (実行結果) AUTH_ROLE_ID
AROA1234567890EXAMPLE
```

上記で CMK 作成のための下準備は完了です。  
以下コマンドで、test_user1 専用の CMK を作成します。前述の通りに キーポリシーを指定します。

```Bash
# CMK・キーポリシーを作成
$ KEY_ID=$(aws kms create-key \
  --policy \
'{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Enable IAM policies",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::'${ACCOUNT_ID}':root"
      },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "Allow a specific cognito user to use the key",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "kms:Describe*",
        "kms:Get*",
        "kms:List*"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "cognito-identity.amazonaws.com:sub": "'${IDENTITY_ID}'",
          "aws:userid": "'${AUTH_ROLE_ID}':CognitoIdentityCredentials"
        }
      }
    }
  ]
}' \
  --query "KeyMetadata.KeyId" \
  --output text) && echo ${KEY_ID}
```

```Bash: (実行結果) KEY_ID
b8a9477d-836c-491f-857e-07937918959b
```

以上で、Cognito ユーザと専用の CMK 作成は完了です。

# アクセステスト

想定通りの制御ができているか、以下2種のテストをします。

1. 正常系:  
  Cognito ユーザー「test_user1」が、専用の CMK に**アクセスできる**ことの確認
2. 異常系:  
  他の Cognito ユーザーが、test_user1 専用の CMK に**アクセスできない**ことの確認

## 1. 正常系

test_user1 が専用の CMK にアクセスできることを確認します。  
まずは、test_user1 のクレデンシャルを取得・設定します。  
※トークンなどは、構築時に取得したコマンドと同じなので省略

```Bash
# クレデンシャル取得
$ CREDENTIALS=$(aws cognito-identity get-credentials-for-identity \
  --identity-id ${IDENTITY_ID} \
  --logins "${COGNITO_USER_POOL}=${ID_TOKEN}")

# クレデンシャルを設定
$ export AWS_ACCESS_KEY_ID=`echo "${CREDENTIALS}" | jq -r ".Credentials.AccessKeyId"`
$ export AWS_SECRET_ACCESS_KEY=`echo "${CREDENTIALS}" | jq -r ".Credentials.SecretKey"`
$ export AWS_SECURITY_TOKEN=`echo "${CREDENTIALS}" | jq -r ".Credentials.SessionToken"`
$ aws sts get-caller-identity
```

```Bash: (実行結果)
{
  "UserId": "AROA1234567890EXAMPLE:CognitoIdentityCredentials",
  "Account": "012345678901",
  "Arn": "arn:aws:sts::012345678901:assumed-role/amplify-react-dev-01234-authRole/CognitoIdentityCredentials"
}
```

上記クレデンシャルで、test_user1 専用の CMK にアクセスしてみます。

```Bash
# CMKの情報を取得
$ aws kms describe-key --key-id ${KEY_ID}
```

```Bash: (実行結果)
{
  "KeyMetadata": {
    "AWSAccountId": "012345678901",
    "KeyId": "b8a9477d-836c-491f-857e-07937918959b",
    "Arn": "arn:aws:kms:ap-northeast-1:012345678901:key/b8a9477d-836c-491f-857e-07937918959b",
    "CreationDate": "2023-03-13T10:14:42.260000+00:00",
    "Enabled": true,
    "Description": "",
    "KeyUsage": "ENCRYPT_DECRYPT",
    "KeyState": "Enabled",
    "Origin": "AWS_KMS",
    "KeyManager": "CUSTOMER",
    "CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
    "KeySpec": "SYMMETRIC_DEFAULT",
    "EncryptionAlgorithms": [
      "SYMMETRIC_DEFAULT"
    ],
    "MultiRegion": false
  }
}
```

無事にアクセスすることができました。

## 2. 異常系

他の Cognito ユーザーを作って、前述の CMK にアクセスできないことを確認してみます。  
まずは、「test_user2」を作成します。

```Bash
# クレデンシャルのリセット
$ export -n AWS_ACCESS_KEY_ID
$ export -n AWS_SECRET_ACCESS_KEY
$ export -n AWS_SECURITY_TOKEN

# 他ユーザー情報を定義
$ USER_NAME=test_user2
$ PASSWORD="Password@2"

# 他ユーザー「test_user2」を作成
$ aws cognito-idp admin-create-user \
  --user-pool-id ${USER_POOL_ID} \
  --username ${USER_NAME} \
  --user-attributes Name=email,Value="test2@sample.com" Name=email_verified,Value=true \
  --message-action SUPPRESS > /dev/null
$ aws cognito-idp admin-set-user-password \
  --user-pool-id ${USER_POOL_ID} \
  --username ${USER_NAME} \
  --password ${PASSWORD} \
  --permanent
```

次に、test_user2 のクレデンシャルを取得・設定します。

```Bash
# 認証・認可
$ ID_TOKEN=$(aws cognito-idp admin-initiate-auth \
  --user-pool-id ${USER_POOL_ID} \
  --client-id ${CLIENT_ID} \
  --auth-flow ADMIN_NO_SRP_AUTH \
  --auth-parameters "USERNAME=${USER_NAME},PASSWORD=${PASSWORD}" \
  --query "AuthenticationResult.IdToken" \
  --output text)
$ IDENTITY_ID=$(aws cognito-identity get-id \
  --identity-pool-id ${IDENTITY_POOL_ID} \
  --logins "${COGNITO_USER_POOL}=${ID_TOKEN}" \
  --query "IdentityId" \
  --output text)
$ CREDENTIALS=$(aws cognito-identity get-credentials-for-identity \
  --identity-id ${IDENTITY_ID} \
  --logins "${COGNITO_USER_POOL}=${ID_TOKEN}")

# クレデンシャルを設定
$ export AWS_ACCESS_KEY_ID=`echo "${CREDENTIALS}" | jq -r ".Credentials.AccessKeyId"`
$ export AWS_SECRET_ACCESS_KEY=`echo "${CREDENTIALS}" | jq -r ".Credentials.SecretKey"`
$ export AWS_SECURITY_TOKEN=`echo "${CREDENTIALS}" | jq -r ".Credentials.SessionToken"`
$ aws sts get-caller-identity
```

```Bash: (実行結果)
{
  "UserId": "AROA1234567890EXAMPLE:CognitoIdentityCredentials",
  "Account": "012345678901",
  "Arn": "arn:aws:sts::012345678901:assumed-role/amplify-react-dev-01234-authRole/CognitoIdentityCredentials"
}
```

get-caller-identity の実行結果は、test_user1 と同じです。この点については後ほど触れます。  
上記クレデンシャルで、test_user1 専用の CMK にアクセスしてみます。

```Bash 
# CMKの情報を取得
$ aws kms describe-key --key-id ${KEY_ID}
```

```Bash: (実行結果)
An error occurred (AccessDeniedException) when calling the DescribeKey operation: User: arn:aws:sts::012345678901:assumed-role/amplify-react-dev-01234-authRole/CognitoIdentityCredentials is not authorized to perform: kms:DescribeKey on resource: arn:aws:kms:ap-northeast-1:012345678901:key/b8a9477d-836c-491f-857e-07937918959b because no identity-based policy allows the kms:DescribeKey action
```

想定通り、アクセス拒否されました。

## 考察

test_user1 と test_user2 で get-caller-identity の結果が同じなのに、CMK のアクセス結果が異なるのはなぜでしょうか。  
まずは、test_user1 のリクエストに関する CloudTrail ログをみてみます。

```typescript: (抜粋表示) test_user1 の CloudTrail ログ 
{
  "eventVersion": "1.08",
  "userIdentity": {
    "type": "AssumedRole",
    "principalId": "AROA1234567890EXAMPLE:CognitoIdentityCredentials",
    "sessionContext": {
      "sessionIssuer": {
        "arn": "arn:aws:iam::012345678901:role/amplify-react-dev-01234-authRole"
      },
      "webIdFederationData": {
        "federatedProvider": "cognito-identity.amazonaws.com",
        "attributes": {
          "cognito-identity.amazonaws.com:aud": "ap-northeast-1:11111111-1111-1111-1111-111111111111",
          "cognito-identity.amazonaws.com:sub": "ap-northeast-1:1234abcd-a123-b234-c345-222222222222"
        }
      }
    }
  },
  "eventSource": "kms.amazonaws.com",
  "eventName": "DescribeKey"
}
```

上記の通り、Cognito 経由のリクエストには、WebIdFederationData が含まれています。  
次に、AccessDenied されたtest_user2 のリクエストに関する CloudTrail ログをみてみます。

```diff_typescript: (抜粋表示) test_user2 の CloudTrail ログ 
{
  "eventVersion": "1.08",
  "userIdentity": {
    "type": "AssumedRole",
    "principalId": "AROA1234567890EXAMPLE:CognitoIdentityCredentials",
    "sessionContext": {
      "sessionIssuer": {
        "arn": "arn:aws:iam::012345678901:role/amplify-react-dev-01234-authRole"
      },
      "webIdFederationData": {
        "federatedProvider": "cognito-identity.amazonaws.com",
        "attributes": {
          "cognito-identity.amazonaws.com:aud": "ap-northeast-1:11111111-1111-1111-1111-111111111111",
-         "cognito-identity.amazonaws.com:sub": "ap-northeast-1:1234abcd-a123-b234-c345-222222222222"
+         "cognito-identity.amazonaws.com:sub": "ap-northeast-1:9876abcd-x000-y111-z222-333333333333"
        }
      }
    }
  },
  "eventSource": "kms.amazonaws.com",
  "eventName": "DescribeKey",
+ "errorCode": "AccessDenied"
}
```

:::note
test_user1 と test_user2 の各リクエストで、cognito-identity.amazonaws.com:sub の値が異なっています。  
この値は、Cognito ID プールに紐づく IdentityId であり、キーポリシーでリクエスト元の Cognito ユーザーを識別することができます。
:::

ちなみに、cognito-identity.amazonaws.com:aud は、Cognito ID プール自体の ID になります。  
この値は、認証済みユーザー用の IAM ロールにおける信頼ポリシーでも使用されています。  
[イメージ図](#実現したいこと)の通り、ID プールと IAM ロールがきちんと紐づいている証ですね。

<details><summary>信頼ポリシー例</summary>

```json: amplify-react-dev-01234-authRole
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "cognito-identity.amazonaws.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "cognito-identity.amazonaws.com:aud": "ap-northeast-1:11111111-1111-1111-1111-111111111111"
        },
        "ForAnyValue:StringLike": {
          "cognito-identity.amazonaws.com:amr": "authenticated"
        }
      }
    }
  ]
}
```

</details>

# おわりに

本記事では、Cognito で認証したユーザーに対して、キーポリシーを使って専用 CMK へアクセス許可する方法をご紹介しました。  

CLI では Cognito の挙動が分かりやすく、本記事の執筆を通じて、筆者自身も知識の整理ができました。本記事がどなたかのお役に立てれば、幸いです。

# 参考情報

- [AWS CLIで動かして学ぶCognito IDプールを利用したAWSの一時クレデンシャルキー発行](https://dev.classmethod.jp/articles/get-aws-temporary-security-credentials-with-cognito-id-pool-by-aws-cli/)
- [AWS Cognitoで、AWS CLI でユーザを作りパスワードを設定してCONFIRMEDにする](https://zenn.dev/ytkhs/articles/efc0a777a73f15)
- [フルスタック React アプリケーションを構築する](https://aws.amazon.com/jp/getting-started/hands-on/build-react-app-amplify-graphql/)