こしあん
2025-01-08

KMSに対して一時的なアクセス許可をもたせるKMS Grantを試す


7{icon} {views}


KMS Grantを利用することで、キーのポリシーを変更せずに特定ロールへ復号権限を一時的に付与できる。TerraformやAWS CLIを使えば、ロールBのように従来権限が不足しているロールでも簡単にS3の暗号化オブジェクトを取得できるようになる。

はじめに

  • KMSに対して一時的なアクセス権限をもたせる「KMS Grant」を試してみた
  • これは、一時的にKMSで暗号化しているコンテンツ(例:カスタマーマネージドキーで暗号化されているS3バケット)にアクセスする必要がある場合に、KMSのキーポリシーをいじらずに一時的なアクセス権限をもたせるというもの
  • S3の署名付きURLとは異なり、有効期限を指定できなくGrant自体を手動で削除しないといけないのが??となるが、一時権限はこれで付与するのがベストプラクティスなので試してみた

やること

S3バケットとKMS

  • カスタマーマネージドキー(KMS)を作成
  • S3バケットの暗号化に作成したカスタマーマネージドキーを紐づける
  • ローカルから適当なファイルtest.jsonをアップロード

Assume Role

  • テスト用に簡単な例として以下を作成
    • ロールA, Bの2個を定義し、それぞれにS3のフルアクセス権限をもたせる
    • KMSのキーポリシーを定義し、ロールAのみ復号の権限をもたせる
  • ローカルからロールA, ロールBをAssumeしてS3にアップロードしたファイルがダウンロードできるかどうかを確かめる
    • ロールBはKMSの権限が不足しているので、ロールAのAssumeではS3のファイルがダウンロードできるが、ロールBではダウンロードできない
  • そのあとロールBに対してKMS Grantを付与し、ダウンロードできるかどうか確かめる

Terraformのコード

カスタマーマネージドキー、S3バケットとオブジェクト、2個のロールをサクッと作る

  • ロールのA, BはAssume Roleを想定し、Assumeできる対象はテスト目的で全員にしている。ちゃんとした開発ならもっとちゃんと絞ること
  • KMSの削除は「削除待機」という状態になってすぐ削除されないので、テスト目的は最短で削除できるようにdeletion_window_in_daysを7日(最短)に設定
  • KMSのキーポリシーはロールAのみ設定する
  • S3はカスタマーマネージドキーによる暗号化を有効にし、test.jsonをアップロードしている。ここからファイルをダウンロードするには以下の2点が必要
    • IAMロール側でのS3のダウンロード可能なポリシー(設定済み)
    • KMSのキーポリシー側での、復号化のポリシー(ロールAのみ設定)
data "aws_caller_identity" "current" {}

# IAM ロールAとロールBの作成(Assume role ポリシーに制限を設けない)
resource "aws_iam_role" "roleA" {
  name = "RoleA"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect    = "Allow",
      Principal = { AWS = "*" },
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role" "roleB" {
  name = "RoleB"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect    = "Allow",
      Principal = { AWS = "*" },
      Action    = "sts:AssumeRole"
    }]
  })
}

# IAM ポリシー:S3に対する全アクセス権限
data "aws_iam_policy" "s3_full" {
  arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}

# ロールA、ロールBにS3アクセス権限を付与
resource "aws_iam_role_policy_attachment" "roleA_s3" {
  role       = aws_iam_role.roleA.name
  policy_arn = data.aws_iam_policy.s3_full.arn
}

resource "aws_iam_role_policy_attachment" "roleB_s3" {
  role       = aws_iam_role.roleB.name
  policy_arn = data.aws_iam_policy.s3_full.arn
}

# カスタマーマネージドのKMSキーを作成し、ロールAにのみ復号権限を付与
resource "aws_kms_key" "example" {
  description             = "Customer managed KMS key for S3 encryption"
  deletion_window_in_days = 7


  policy = jsonencode({
    Version = "2012-10-17",
    Id      = "key-default-1",
    Statement = [
      # ルートユーザーに対する全権限の許可
      {
        Sid       = "EnableRootPermissions",
        Effect    = "Allow",
        Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" },
        Action    = "kms:*",
        Resource  = "*"
      },
      # ロールAに対する必要なKMS権限の付与(復号含む)
      {
        Sid       = "AllowRoleAUsage",
        Effect    = "Allow",
        Principal = { AWS = aws_iam_role.roleA.arn },
        Action = [
          "kms:Decrypt",
          "kms:DescribeKey",
          "kms:Encrypt",
          "kms:GenerateDataKey*"
        ],
        Resource = "*"
      }
      # 注: RoleBにはKMSの使用権限を付与していないため、復号時に失敗します
    ]
  })
}

resource "aws_kms_alias" "myalias" {
  name          = "alias/my-kms-grant-example-key"
  target_key_id = aws_kms_key.example.id
}

# S3バケットを作成
resource "aws_s3_bucket" "example" {
  bucket        = "my-example-bucket-for-assume-role-test"
  force_destroy = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "example" {
  bucket = aws_s3_bucket.example.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.example.arn
    }
  }
}

# ローカルファイルをS3にアップロード
resource "aws_s3_object" "file_upload" {
  bucket                 = aws_s3_bucket.example.id
  key                    = "test.json"
  source                 = "${path.module}/test.json"
  server_side_encryption = "aws:kms"
  kms_key_id             = aws_kms_key.example.arn
  content_type           = "application/json"
}

ローカルからのテストコード

ローカルからboto3でダウンロードを試してみる。ロールAは成功するが、ロールBはダウンロード失敗するはず。

import boto3
import io
from botocore.exceptions import ClientError

boto_session = boto3.Session(profile_name="hogehoge")

def test_assume_role(role_name="RoleA"):
    # 使用するロール名、セッション名、S3情報を指定
    role_session_name = "DownloadSession"
    bucket_name = "my-example-bucket-for-assume-role-test"  # 実際のバケット名に置き換え
    object_key = "test.json"                                # ダウンロード対象のオブジェクトキー

    # IAMクライアントを作成し、ロール名からARNを取得
    iam_client = boto_session.client('iam')
    try:
        response = iam_client.get_role(RoleName=role_name)
        role_arn = response['Role']['Arn']
    except ClientError as e:
        print(f"Error getting role ARN: {e}")
        exit(1)

    # STSクライアントを作成してロールをAssume
    sts_client = boto_session.client('sts')
    try:
        assumed_role = sts_client.assume_role(
            RoleArn=role_arn,
            RoleSessionName=role_session_name
        )
    except ClientError as e:
        print(f"Error assuming role: {e}")
        exit(1)

    print(f"Assume role : {role_name}")

    # Assumeしたロールの一時的な認証情報を取得
    credentials = assumed_role['Credentials']

    # 一時的な認証情報を使ってS3クライアントを作成
    s3_client = boto_session.client(
        's3',
        aws_access_key_id=credentials['AccessKeyId'],
        aws_secret_access_key=credentials['SecretAccessKey'],
        aws_session_token=credentials['SessionToken']
    )

    # S3からファイルをダウンロード
    try:
        with io.BytesIO() as file_buffer:
            s3_client.download_fileobj(bucket_name, object_key, file_buffer)
            file_content = file_buffer.getvalue().decode('utf-8')
        print(f"File downloaded successfully !")
        print(file_content)
    except ClientError as e:
        print(f"Error downloading file: {e}")

if __name__ == "__main__":
    test_assume_role(role_name="RoleA")

ロールAの場合(成功)

Assume role : RoleA
File downloaded successfully !
{
    "id": 123456,
    "message": "Hello, KMS Grant!"
}

ロールBの場合(権限不足により失敗)

Assume role : RoleB
Error downloading file: An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::123456789012:assumed-role/RoleB/DownloadSession is not authorized to perform: kms:Decrypt on resource: arn:aws:kms:ap-northeast-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab because no identity-based policy allows the kms:Decrypt action

ここまでは想定された挙動。

KMS Grantを付与

AWS CLIでやるのが一般的かもしれないが、Terraformのほうが書くの楽だったのでgrant.tfみたいな別ファイルを作って実装

# RoleBに一時的にDecrypt権限を付与するKMSグラント
resource "aws_kms_grant" "roleB_decrypt_grant" {
  key_id            = aws_kms_key.example.key_id
  grantee_principal = aws_iam_role.roleB.arn
  retire_on_delete  = false

  # ここで指定する操作を許可
  operations = [
    "Decrypt"
  ]
}

grantee_principalが付与する対象で、今回はDecryptのみ許可している。retire_on_deleteはリソースがdestroyされたときに、Grantを廃止する(=true)か、取り消すか(=false)の違いだが、trueにしたときにうまくいかなかったのでfalseにした。

Grantを別途applyすると、ロールBをAssumeしてもダウンロードできるようになる。

Assume role : RoleB
File downloaded successfully !
{
    "id": 123456,
    "message": "Hello, KMS Grant!"
}

Grant一覧の確認

AWS CLIから確認できる。

aws kms list-grants --key-id 1234abcd-12ab-34cd-56ef-1234567890ab --profile hogehoge

以下のように表示されたら、Grantが付与されていない状態。なにか表示されたらGrantがある状態。

{
    "Grants": []
}

Grantの付与をAWS CLIで行う場合

以下ChatGPTのコピペ


1. RoleB の ARN を取得

RoleB の ARN が既にわからない場合は、以下のコマンドで取得できます:

aws iam get-role --role-name RoleB --query "Role.Arn" --output text

この出力を後続のコマンドで使用します。

2. KMS キーの情報を取得

対象の KMS キーの ID または ARN を確認します。もしわからない場合は、以下のコマンドで一覧から探すことができます:

aws kms list-keys --query "Keys[*].KeyId" --output text

特定のキーの詳細を知りたい場合:

aws kms describe-key --key-id <キーIDまたはARN>

3. RoleB に一時的なキーグラントを付与

以下のコマンドを使用して、RoleB に対して指定した操作を許可するグラントを作成します。この例では「復号 (Decrypt)」操作を許可するグラントを作成しますが、必要に応じて他の操作も追加できます。

aws kms create-grant \
    --key-id <キーIDまたはARN> \
    --grantee-principal <RoleBのARN> \
    --operations Decrypt

例えば、キーIDが 1234abcd-12ab-34cd-56ef-1234567890ab、RoleBのARNが arn:aws:iam::123456789012:role/RoleB の場合:

aws kms create-grant \
    --key-id 1234abcd-12ab-34cd-56ef-1234567890ab \
    --grantee-principal arn:aws:iam::123456789012:role/RoleB \
    --operations Decrypt

4. グラントの確認

作成されたグラントを確認するには、以下のコマンドを使用します:

aws kms list-grants --key-id <キーIDまたはARN>

注意点

  • グラントは自動的に期限切れになるわけではありません。不要になった場合は、グラントIDを指定して削除する必要があります。
  • グラント作成時に許可する操作(この例ではDecrypt)を適切に設定します。他の必要な操作があれば --operations オプションに追加してください。
  • この方法は一時的なアクセス許可を与えるためのものであり、グラントの有効期間や条件は設定できません。より詳細な条件設定が必要な場合は、KMSポリシーやIAMポリシーでの制御を検討してください。

所感

  • 試験問題に出てきたからやってみたものの、自動的に有効期限切れになるわけではないのがうーんという感じ
  • IaCで管理するのがいいのだろうか。細かな条件を指定できるのがグラント強いが、キーポリシーをいじるのとあんまり変わらないような気がするが…
  • S3バケットに対して一時的な閲覧権限を与えるというのは割と使えそう


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です