AWS STS のセッションタグを使用したDynamoDBとS3の動的なアクセス制御

記事タイトルとURLをコピーする

こんにちは。
アプリケーションサービス本部 DevOps担当の兼安です。
久しぶりにプログラムの話題を取り上げます。
今回は、AWSのセッションタグを使用して、DynamoDBとS3に対する動的なアクセス制御を実装する方法について紹介します。

概要

AWS セッションタグは、一時的な認証情報にタグを付与する機能です。
これを利用することで動的かつ行レベルのようなきめ細かいアクセス制御を実現することができます。
本記事では、セッションタグを活用してDynamoDBとS3に対して動的なアクセスを制御する方法を解説します。

アプリケーションにおける検索条件

アプリケーションにデータベースやストレージがある場合、その中には複数のデータが混在していることが一般的です。
マルチテナントやそれに近い環境では、その傾向はより顕著になります。
複数のデータが混在している場合、権限によっては特定のデータのみを検索できるようにする必要があるでしょう。
この時、プログラムでは、データアクセス時に検索条件を設定することが一般的です。

しかし、データアクセスする箇所が多数ある場合、必然的に検索条件を設定する箇所も多数存在することになります。
これは、バグの温床となり、セキュリティリスクを高める要因となります。

flowchart TD
    A[プログラム] --> B[データアクセス<br>検索条件設定]
    A --> C[データアクセス<br>検索条件設定]
    A --> D[データアクセス<br>検索条件設定]
    B -->|データ取得| E[データベース・ストレージ]
    C -->|データ取得| E
    D -->|データ取得| E

本記事で述べているAWS STSのセッションタグを使用したアクセス制御を用いると、プログラムの冒頭でアクセス制御を設定することで、以降のデータアクセスにおいては、検索条件の設定を省くことができます。
アクセス制御を設定する箇所は1箇所のみとなり、セキュリティリスクを低減することが可能です。
この方式は少々複雑ではありますが、マルチテナント環境やそれに準ずる環境においては、有効な手法となり得ます。

flowchart TD
    A[プログラム] --> B[セッションタグを使用した<br>アクセス制御設定]
    B --> C[データアクセス]
    B --> D[データアクセス]
    B --> E[データアクセス]
    C -->|データ取得| F[データベース・ストレージ]
    D -->|データ取得| F
    E -->|データ取得| F

次のセクションから、セッションタグを使用したアクセス制御の具体的な実装方法と、DynamoDBおよびS3に対するアクセス制御の例を示します。

AWS STSのセッションタグによる動的なアクセス制御

セッションタグは、STS(Security Token Service)のAssumeRole操作時に設定できるタグで、一時的な認証情報に付与されます。
これにより、IAMポリシーの条件式で${aws:PrincipalTag/<セッションタグのタグキー>}のように記載することで、動的なアクセス制御を実現できます。

手順は以下の通りです。

  1. セッションタグを条件式に組み込んだIAMポリシーを持つIAMロールを作成します。
  2. Lambda関数などの実行環境で、AssumeRoleで一時的なセッションを取得します。 この時、セッションタグを指定します。
  3. 取得した一時的なセッションを使用して、DynamoDBやS3などのAWSサービスにアクセスします。
  4. DynamoDBやS3にアクセスする際、IAMポリシーの条件式の部分にセッションタグが適用され、アクセス制御が行われます。

IAMロールの準備

今回の仕組みを実装するためには、IAMロールが2つ必要です。
一つは、一般的にLambda関数を実行するためのIAMロール。
こちらは、CloudWatch Logsへの書き込み権限など、Lambda関数の実行に必要な権限を持つロールです。
AWSマネジメントコンソールからLambda関数を作成する際に自動的に作成されるものです。

もう一つは、セッションタグを使用したアクセス制御を行うためのIAMロールです。
こちらを今から作成します。
このロールには、DynamoDBやS3に対するアクセス権限を含め、セッションタグを条件にしたポリシーを設定します。

DynamoDB用のポリシー

DynamoDB用のポリシーのdynamodb:LeadingKeysは、テーブルの最初のキー属性、つまりパーティションキーを表します。
このポリシーでは、セッションタグで指定された値と一致するパーティションキーを持つアイテムに対してのみ、GetItemおよびPutItemアクションが許可されます。

{
  "Effect": "Allow",
  "Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
  "Resource": "arn:aws:dynamodb:ap-northeast-1:<AWS アカウントID>:table/<テーブル名>",
  "Condition": {
    "ForAllValues:StringEquals": {
      "dynamodb:LeadingKeys": ["${aws:PrincipalTag/SESSION_TAG_VALUE}"]
    }
  }
}

S3用のポリシー

Resourceで指定しているARNのプレフィックスに想定する部分を${aws:PrincipalTag/SESSION_TAG_VALUE}としています。
ここが、セッションタグで指定された値に置換されることにより、特定のプレフィックスを持つオブジェクトへのアクセスが制御されます。

{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::<S3バケット名>/${aws:PrincipalTag/SESSION_TAG_VALUE}/*"
}

IAMロールの信頼ポリシー

このポリシーを持ったIAMロールを作成します。
この時、IAMロールには以下の信頼ポリシーを設定します。
sts:TagSessionアクションを許可することで、セッションタグを設定できるようにしています。
Principalには、セッションタグ用のIAMロールではなく、Lambda関数の実行ロールのARNを指定します。
これにより、Lambda関数がセッションタグを使用するためのIAMロールをAssumeRoleできるようになります。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<AWS アカウントID>:role/<Lambda関数の実行ロール名>"
      },
      "Action": [
        "sts:AssumeRole",
        "sts:TagSession"
      ]
    }
  ]
}

Pythonを使用したLambda関数の例

PythonによるLambda関数の例を以下に示します。
Lambda関数の冒頭で、セッションタグのために作成しておいたIAMロールを、セッションタグを指定しながらAssumeRoleします。
これにより、一時的なセッションを取得します。
一時的なセッションには、セッションタグが含まれます。

取得した一時的なセッションを通して、DynamoDBやS3のクライアントを作成します。
これによりデータの書き込みや読み取りを行った時に、セッションタグが適用され、アクセス制御が行われます。

import boto3
import json

def lambda_handler(event, context):
    session_tag_value = event.get('session_tag_value')
    sts_client = boto3.client('sts')

    # セッションタグを指定してAssumeRole
    response = sts_client.assume_role(
        RoleArn='arn:aws:iam::<AWS アカウントID>:role/<ロール名>',
        RoleSessionName='任意のセッション名',
        Tags=[
            {
                'Key': 'SESSION_TAG_VALUE',
                'Value': session_tag_value
            }
        ]
    )

    credentials = response['Credentials']

    # boto3セッションを作成
    session = boto3.Session(
        aws_access_key_id=credentials['AccessKeyId'],
        aws_secret_access_key=credentials['SecretAccessKey'],
        aws_session_token=credentials['SessionToken']
    )

    # DynamoDBへのアクセス
    dynamodb = session.resource('dynamodb')
    table = dynamodb.Table('<テーブル名>')
    try:
        table.put_item(
            Item={
                'partition_key': session_tag_value,
                'data': 'example data'
            }
        )
        response = table.get_item(
            Key={'partition_key': session_tag_value}
        )
        print(response['Item'])
    except Exception as e:
        print(f"DynamoDB access failed: {e}")

    # S3へのアクセス
    s3 = session.client('s3')
    try:
        s3.put_object(
            Bucket='<S3バケット名>',
            Key=f"{session_tag_value}/your-object-key",
            Body=session_tag_value.encode('utf-8')
        )
        s3_response = s3.get_object(
            Bucket='<S3バケット名>',
            Key=f"{session_tag_value}/your-object-key"
        )
        print(f"S3 object content: {s3_response['Body'].read().decode('utf-8')}")
    except Exception as e:
        print(f"S3 access failed: {e}")

    return {
        'statusCode': 200,
        'body': json.dumps('Session tag access control test completed')
    }

実行結果はこちらです。

START RequestId: efa1f6b4-873f-4017-b4c2-f9674c4bfcb0 Version: $LATEST
{'partition_key': 'value1', 'data': 'example data'}
S3 object content: value1
END RequestId: efa1f6b4-873f-4017-b4c2-f9674c4bfcb0
REPORT RequestId: efa1f6b4-873f-4017-b4c2-f9674c4bfcb0  Duration: 5424.94 ms  Billed Duration: 5425 ms  Memory Size: 128 MB Max Memory Used: 99 MB  Init Duration: 294.95 ms

このコードに、セッションタグに合致しないデータへのアクセスを試みるコードを追加することで、セッションタグによるアクセス制御の動作を確認できます。

    # セッションタグに合致しないデータへのアクセス試行
    try:
        # DynamoDBでセッションタグに合致しないデータを取得
        response = table.get_item(
            Key={'partition_key': 'other-partition-key'}
        )
        if 'Item' in response:
            print("Unexpectedly accessed data with different partition key")
        else:
            print("Access denied as expected for different partition key")
    except Exception as e:
        print(f"Access denied for different partition key: {e}")

    try:
        # S3でセッションタグに合致しないオブジェクトを取得
        s3_response = s3.get_object(
            Bucket='<S3バケット名>',
            Key='other-prefix/your-object-key'
        )
        print("Unexpectedly accessed S3 object with different prefix")
    except Exception as e:
        print(f"Access denied for different S3 prefix: {e}")

実行結果は以下の通りです。
DynamoDB・S3ともに、セッションタグに合致しないデータへのアクセスが拒否されていることが確認できます。
なお、エラーメッセージにs3:ListBucketとありますが、今回のサンプルコードには直接は関係ないはずです。
boto3のSDKがGetObject失敗時にListBucketを試みているのではと思います。
(もしはっきりとしたことがわかれば追記します)

START RequestId: 40b82b0b-b69b-49a6-bd58-11b701fc5ad9 Version: $LATEST
{'partition_key': 'value1', 'data': 'example data'}
S3 object content: value1
Access denied for different partition key: An error occurred (AccessDeniedException) when calling the GetItem operation: User: arn:aws:sts::<AWSアカウントID>:assumed-role/<IAMロール>/<任意のセッション名> is not authorized to perform: dynamodb:GetItem on resource: arn:aws:dynamodb:ap-northeast-1:<AWSアカウントID>:table/<DynamoDBテーブル名> because no identity-based policy allows the dynamodb:GetItem action
Access denied for different S3 prefix: An error occurred (AccessDenied) when calling the GetObject operation: User: arn:aws:sts::<AWSアカウントID>:assumed-role/<IAMロール>/<任意のセッション名> is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::<S3バケット名>" because no identity-based policy allows the s3:ListBucket action
END RequestId: 40b82b0b-b69b-49a6-bd58-11b701fc5ad9
REPORT RequestId: 40b82b0b-b69b-49a6-bd58-11b701fc5ad9  Duration: 3307.36 ms  Billed Duration: 3308 ms  Memory Size: 128 MB Max Memory Used: 114 MB

AWS STSのセッションタグを用いたアクセス制御のシーケンス図

上述のLambda関数の実行フローをシーケンス図で示します。

シーケンス図

セッションタグを使用した動的なアクセス制御のデメリット

ここまでセッションタグを使用した動的なアクセス制御を説明しましたが、デメリットがあります。
まず、プログラムの冒頭でAssumeRoleを実行し、セッションタグを設定した一時的なセッションを取得していますが、DynamoDBやS3にアクセスする際に、この一時的なセッションを使わない実装をしてしまうと、セッションタグが適用されず、アクセス制御が機能しません。
見るべきポイントは明確ではありますが、コードレビューなどでフォローする必要があります。

次に、仕組み自体がお世辞にもわかりやすいとは言えません。
アプリケーションプログラムとIAMポリシーの合わせ技となっていることが、アプリとインフラのチーム間をまたいだ問題になる可能性があります。
わかりやすさを求める場合は、テナント単位でDynamoDBテーブルや環境を丸ごと分けるような別のアプローチを検討することも重要です。

まとめ

AWS STSのセッションタグを使用することで、DynamoDBやS3に対して動的なアクセス制御を実現できます。
マルチテナント環境やそれに準ずる環境において、個々のデータアクセス処理の作り込みを減らし、セキュリティリスクを低減することが可能です。
一方で、仕組み自体が複雑であるため、実装には注意が必要です。
トリッキーな実装であることは意識しておく必要があります。

参考リンク

兼安 聡(執筆記事の一覧)

アプリケーションサービス部 DS3課所属
2025 Japan AWS Top Engineers (AI/ML Data Engineer)
2025 Japan AWS All Certifications Engineers
2025 AWS Community Builders
Certified ScrumMaster
PMP
広島在住です。今日も明日も修行中です。