IAMロールのタグとLeadingKeysを用いたDynamoDBへのABAC実装
Posted On 2025-01-27
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-a
とdynamo-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の中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー