こしあん
2025-01-27

IAMロールのタグとLeadingKeysを用いたDynamoDBへのABAC実装


36{icon} {views}


IAMロールに付与したタグとDynamoDBのパーティションキーをConditionで照合し、特定ロールのみアイテムの書き込みを許可する設定をTerraformで実装しています。タグによる属性ベースのアクセス制御により、プロジェクト単位で権限を分離できる仕組みが確認されました。

はじめに

  • SAP勉強してて「dynamodb:LeadingKeys」を使うとDynamoDBへの書き込み権限をIAMポリシーで制御できるよというもの
  • わかりやすい例は、aws:usernameとしてIAMユーザーの名前で絞るもの。これでパーティションキーがユーザー名に一致するレコード編集できなくなる。
  • 例えばユーザー名がjohn.smithさんだったら、パーティションキーにdonald.trumpと入れたレコードを追加・編集することは許可されない。これは仮に対象のアクションを許可していたとしてもレコード単位で権限が絞れる。
  • このためにIAMユーザー追加するのもどうかな→Cognitoを使う例もなんか面倒だな→となったので、今回はIAMロールにタグを付けて、Assume roleして、そのタグが一致したレコードのみ書き込み許可するというABAC(Attribute-Based Access Control、属性ベースのアクセス制御)の設定でやってみる。結構わかりやすい結果になった
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": ["${aws:username}"]
        }
      }

参考情報

以下が参考になる

Terraformのコード

短いのでTerraformのコードを直接貼る。

  • 2つのロール:dynamo-db-leading-keys-project-adynamo-db-leading-keys-project-bを定義し、これらをローカルからAssume roleしてテストする
  • このロールに紐づいたタグProjectの名前で判定して、書き込めるDynamoDBのレコードを決める
  • ロールはプロジェクト単位で作っているが、ポリシーは一括管理している
  • AWSのリソースに紐づいたタグはaws:PrincipalTag/TagNameで取れるので、ここを/Projectにすればプロジェクトタグを取れるという仕組み。これとDynamoDBのdynamodb:LeadingKeysと組み合わせれば書き込むレコードを絞れる。
# 現在のアカウント ID を取得
data "aws_caller_identity" "current" {}

# DynamoDB テーブルの作成
resource "aws_dynamodb_table" "leading_keys_table" {
  name         = "leading-keys-table" # テーブル名を適宜変更してください
  billing_mode = "PAY_PER_REQUEST"

  hash_key     = "project_name" # パーティションキーの名前

  attribute {
    name = "project_name"
    type = "S" # パーティションキーの型(S: String, N: Number, etc.)
  }
}

# IAM ロールの作成
resource "aws_iam_role" "leading_keys_role_project_a" {
  name = "dynamo-db-leading-keys-project-a"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        # アカウント内の全ユーザーがAssumeできるようにする
        AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
      }
      Action = "sts:AssumeRole"
    }]
  })

  tags = {
    Project = "project-a"
  }
}

resource "aws_iam_role" "leading_keys_role_project_b" {
  name = "dynamo-db-leading-keys-project-b"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
      }
      Action = "sts:AssumeRole"
    }]
  })

  tags = {
    Project = "project-b"
  }
}


# IAM ポリシーの作成
resource "aws_iam_policy" "leading_keys_policy" {
  name        = "leading-keys-policy"
  description = "Allows DynamoDB operations only on items with partition key matching the project tag name."

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowSessionToAccessOwnerItems"
        Effect = "Allow"
        Action = [
          "dynamodb:GetItem",
          "dynamodb:Query",
          "dynamodb:PutItem",
          "dynamodb:UpdateItem",
          "dynamodb:DeleteItem"
        ]
        Resource  = aws_dynamodb_table.leading_keys_table.arn
        Condition = {
          "ForAllValues:StringEquals": {
            # Terraformの${}による変数展開と衝突しないように、$を2つ重ねてエスケープする
            "dynamodb:LeadingKeys": "$${aws:PrincipalTag/Project}"
          }
        }
      }
    ]
  })
}

# IAM ポリシーをロールにアタッチ
resource "aws_iam_role_policy_attachment" "attach_leading_keys_policy_project_a" {
  role       = aws_iam_role.leading_keys_role_project_a.name
  policy_arn = aws_iam_policy.leading_keys_policy.arn
}

resource "aws_iam_role_policy_attachment" "attach_leading_keys_policy_project_b" {
  role       = aws_iam_role.leading_keys_role_project_b.name
  policy_arn = aws_iam_policy.leading_keys_policy.arn
}

テストコード

ローカルから以下のコードを実行する。以下のような結果になるはずである。

ロール名 / 書き込むproject_name project-a project-b
dynamo-db-leading-keys-project-a
dynamo-db-leading-keys-project-b
  • 縦軸がAssume roleするロール名、横軸がDynamo DBに書き込むプライマリーキー(project_name)
  • 各ロールに紐づいているタグ(Project)ごとに書き込めるかが判定されるというもの
import boto3
from botocore.exceptions import ClientError

# 設定
ROLE_BASENAME = "dynamo-db-leading-keys"   # 作成したIAMロールのプレフィックス名
DYNAMODB_TABLE_NAME = "leading-keys-table" # DynamoDBテーブル名
REGION_NAME = "ap-northeast-1"  # DynamoDBテーブルが存在するリージョン

boto_session = boto3.Session(profile_name="hogehoge")
iam_client = boto_session.client('iam')

def get_role_arn(role_name):
    """
    指定されたロール名からロールのARNを取得します。
    """
    try:
        response = iam_client.get_role(RoleName=role_name)
        role_arn = response['Role']['Arn']
        print(f"Successfully retrieved ARN for role '{role_name}': {role_arn}")
        return role_arn
    except ClientError as e:
        print(f"Error retrieving ARN for role '{role_name}': {e.response['Error']['Message']}")
        return None

def assume_role(role_arn, session_name):
    """
    指定されたロールをAssumeし、一時的な認証情報を取得します。
    """
    sts_client = boto_session.client('sts')
    try:
        response = sts_client.assume_role(
            RoleArn=role_arn,
            RoleSessionName=session_name
        )
        credentials = response['Credentials']
        print(f"Assumed role '{role_arn}' with session name '{session_name}'.")
        return credentials
    except ClientError as e:
        print(f"Error assuming role {role_arn} with session name {session_name}: {e.response['Error']['Message']}")
        return None

def get_dynamodb_client(credentials, region):
    """
    一時的な認証情報を使用してDynamoDBクライアントを作成します。
    """
    dynamodb_client = boto3.client(
        'dynamodb',
        region_name=region,
        aws_access_key_id=credentials['AccessKeyId'],
        aws_secret_access_key=credentials['SecretAccessKey'],
        aws_session_token=credentials['SessionToken']
    )
    return dynamodb_client

def put_item(dynamodb_client, table_name, project_name, item_id, data):
    """
    DynamoDBテーブルにアイテムを追加します。
    """
    try:
        response = dynamodb_client.put_item(
            TableName=table_name,
            Item={
                'project_name': {'S': project_name},
                'item_id': {'S': item_id},
                'data': {'S': data}
            }
        )
        print(f"Successfully added item '{item_id}' for project_name '{project_name}'.")
    except ClientError as e:
        print(f"Failed to add item '{item_id}' for project_name '{project_name}': {e.response['Error']['Message']}")

def main():
    # テストするプロジェクト名
    project_names = ["project-a", "project-b"]

    for project_name in project_names:
        # ロールARNを動的に取得
        role_arn = get_role_arn(ROLE_BASENAME + "-" + project_name)
        if not role_arn:
            print("ロールARNの取得に失敗したため、スクリプトを中止します。")
            return

        # ロールをAssume
        credentials = assume_role(role_arn, "sample_session")
        if not credentials:
            continue  # ロールのAssumeに失敗した場合は次のセッションへ

        # DynamoDBクライアントを取得
        dynamodb = get_dynamodb_client(credentials, REGION_NAME)

        for sub_project_name in project_names:
            # レコードの追加
            if project_name != sub_project_name:
                print(f"\nAttempting to add item with invalid project_name: '{sub_project_name}' (should fail)")
            else:
                print()
            put_item(dynamodb, DYNAMODB_TABLE_NAME, sub_project_name, "Item1", f"Sample Data 1 by {sub_project_name}")
            put_item(dynamodb, DYNAMODB_TABLE_NAME, sub_project_name, "Item2", f"Sample Data 2 by {sub_project_name}")


if __name__ == "__main__":
    main()

結果

想定された結果になった

Successfully retrieved ARN for role 'dynamo-db-leading-keys-project-a': arn:aws:iam::123456789012:role/dynamo-db-leading-keys-project-a
Assumed role 'arn:aws:iam::123456789012:role/dynamo-db-leading-keys-project-a' with session name 'sample_session'.

Successfully added item 'Item1' for project_name 'project-a'.
Successfully added item 'Item2' for project_name 'project-a'.

Attempting to add item with invalid project_name: 'project-b' (should fail)
Failed to add item 'Item1' for project_name 'project-b': User: arn:aws:sts::123456789012:assumed-role/dynamo-db-leading-keys-project-a/sample_session is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:ap-northeast-1:123456789012:table/leading-keys-table because no identity-based policy allows the dynamodb:PutItem action
Failed to add item 'Item2' for project_name 'project-b': User: arn:aws:sts::123456789012:assumed-role/dynamo-db-leading-keys-project-a/sample_session is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:ap-northeast-1:123456789012:table/leading-keys-table because no identity-based policy allows the dynamodb:PutItem action

=====

Successfully retrieved ARN for role 'dynamo-db-leading-keys-project-b': arn:aws:iam::123456789012:role/dynamo-db-leading-keys-project-b
Assumed role 'arn:aws:iam::123456789012:role/dynamo-db-leading-keys-project-b' with session name 'sample_session'.

Attempting to add item with invalid project_name: 'project-a' (should fail)
Failed to add item 'Item1' for project_name 'project-a': User: arn:aws:sts::123456789012:assumed-role/dynamo-db-leading-keys-project-b/sample_session is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:ap-northeast-1:123456789012:table/leading-keys-table because no identity-based policy allows the dynamodb:PutItem action
Failed to add item 'Item2' for project_name 'project-a': User: arn:aws:sts::123456789012:assumed-role/dynamo-db-leading-keys-project-b/sample_session is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:ap-northeast-1:123456789012:table/leading-keys-table because no identity-based policy allows the dynamodb:PutItem action

Successfully added item 'Item1' for project_name 'project-b'.
Successfully added item 'Item2' for project_name 'project-b'.

実際のDynamo DBのレコードを見ると以下の通り

所感

  • うまくいっておおっ!となった。テナント分離で使うことが多そうだけど、思わぬ飛び道具として使えそう
  • Assume roleにしたのは説明をわかりやすくするためで、もっといいやり方はあると思う
  • Cognitoとの連携も使う機会がありそう


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

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

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