AgentCore GatewayのHTTPターゲットにconnectorタイプが追加されたのでAgentCore Memoryを繋いでみた

AgentCore GatewayのHTTPターゲットにconnectorタイプが追加されたのでAgentCore Memoryを繋いでみた

Amazon Bedrock AgentCore Gatewayに新しく追加されたconnectorタイプのHTTPターゲットを試してみました!
2026.08.12

はじめに

こんにちは、スーパーマーケットが大好きなコンサルティング部の神野です。

2026年8月5日のAPIアップデートで、Amazon Bedrock AgentCore Controlに気になる変更が入っていました。

https://awsapichanges.com/archive/changes/f0f528-bedrock-agentcore-control.html

Amazon Bedrock AgentCore Control now supports fine-grained access control through managed AgentCore Gateway HTTP Connectors.

GatewayのHTTPターゲットに connector という新しいタイプが追加され、マネージドコネクタ(AWSが用意した組み込みの接続先)経由のルーティングと細かいアクセス制御ができるようになったようです。いまいちピンとこないですよね・・・

結論としては、AgentCore MemoryをGateway越しに公開できる機能でした!公式ドキュメントも踏まえつつCedarでの認可制御まで検証していきます。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory-gateway-connector.html

これまでGatewayのターゲットについて記事を書いてきましたが、今回のアップデートはこれらの続きにあたる位置づけのように感じました。

アップデート概要

GatewayのHTTPターゲットには、これまで2つのタイプがありました。

タイプ 説明
agentcoreRuntime AgentCore Runtimeのエージェントへルーティング
passthrough 外部HTTPエンドポイントへ直接転送

今回のアップデートで、3つ目のタイプとして connector が追加されました!

タイプ 説明
connector マネージドコネクタ経由でルーティング

ターゲット設定は、connectorIdと、リソースを固定するparametersを渡すだけのシンプルな作りです。

ターゲット設定の構造
{
  "http": {
    "connector": {
      "source": {
        "connectorId": "agentcore-memory"
      },
      "parameters": {
        "memoryId": "<公開したいMemoryのID>"
      }
    }
  }
}

公式のAPI Referenceには既にHttpConnectorTargetConfigurationのページが公開されていて、parametersの説明に下記の記載があります。

The resource parameters for this connector (for example, memoryId). The service validates these parameters against the request path at runtime.

https://docs.aws.amazon.com/bedrock-agentcore-control/latest/APIReference/API_HttpConnectorTargetConfiguration.html

これはparametersに指定したリソースとリクエストパスを実行時に突き合わせて検証してくれる、とのことです。ただしこれはターゲットの接続先を固定する仕組みで、誰がどのアクターやどの操作にアクセスできるかという認可は、後述のPolicy EngineのCedarポリシーが担います。

コネクタというとWeb SearchなどのMCPターゲット用コネクタや、Inferenceターゲットのコネクタ型(Bedrock Mantleなど)が既にありましたが、その仕組みがHTTPターゲットにも広がってきましたね・・・!

前提

検証時の環境です。

項目 内容
リージョン us-east-1
boto3 / botocore botocore 1.43.67(1.43.65以降で今回のAPIに対応)
Gateway 記事内で新規作成(インバウンド認証 AWS_IAM(IAM認証))
Memory 作成済みのAgentCore Memory

今回のAPIはbotocore 1.43.65(2026年8月5日リリース)以降に含まれているため、古いバージョンの方はアップデートしておきましょう!

本記事で使ったコードは下記リポジトリにまとめています。uvのプロジェクトとして依存をuv.lockで固定してあり、cloneして uv run するだけで初回実行時に依存が自動解決されます。記事中のコードは抜粋なので、手元で再現する場合はリポジトリをcloneして進めるのがおすすめです。

https://github.com/yuu551/agentcore-memory-gateway-sample

実行イメージ
git clone https://github.com/yuu551/agentcore-memory-gateway-sample.git
cd agentcore-memory-gateway-sample
uv run scripts/create_gateway.py --name memory-gateway-blog \
  --role-arn arn:aws:iam::<アカウントID>:role/memory-gateway-role

Memoryコネクタの概要

公式ドキュメントによると、HTTPコネクタとして提供されるのはAgentCore Memoryコネクタで、connectorIdは agentcore-memory です。ターゲットにmemoryIDを固定し、Memoryのデータプレーンへの配線とAPIスキーマの提供をコネクタ側がまとめて引き受けてくれます。

対応するのは次の12個のMemoryデータプレーンオペレーションで、それぞれがCedarポリシーのアクションとして公開されます。

これでGateway経由で下記のAPIがアクセスできるようになったと理解するとわかりやすいと思います。

分類 オペレーション
イベント CreateEvent / GetEvent / ListEvents / DeleteEvent / ListSessions / ListActors
長期記憶 RetrieveMemoryRecords / ListMemoryRecords / GetMemoryRecord / DeleteMemoryRecord
抽出ジョブ ListMemoryExtractionJobs / StartMemoryExtractionJob

わかりづらいと思うので早速Gatewayを作って試してみましょう!

Memoryコネクタターゲットを作成する

それでは実際に作成していきます。Gatewayの実行ロール、Gateway本体、ターゲットの順に作ります。

Gateway実行ロールの作成

まずGatewayの実行ロールを作成します。信頼ポリシーで bedrock-agentcore サービスからのAssumeRoleを許可します。

ロールの作成
aws iam create-role \
  --role-name memory-gateway-role \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {"Service": "bedrock-agentcore.amazonaws.com"},
      "Action": "sts:AssumeRole",
      "Condition": {"StringEquals": {"aws:SourceAccount": "<アカウントID>"}}
    }]
  }'

このロールには対象Memoryへアクセスする権限も必要です。

次のインラインポリシーをロールに追加します。

memory-connector-access.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "bedrock-agentcore:GetMemory",
        "bedrock-agentcore:CreateEvent",
        "bedrock-agentcore:GetEvent",
        "bedrock-agentcore:ListEvents",
        "bedrock-agentcore:ListSessions",
        "bedrock-agentcore:ListActors",
        "bedrock-agentcore:GetMemoryRecord",
        "bedrock-agentcore:ListMemoryRecords",
        "bedrock-agentcore:RetrieveMemoryRecords"
      ],
      "Resource": "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:memory/<MemoryのID>"
    }
  ]
}
Memoryアクセス権限の付与
aws iam put-role-policy \
  --role-name memory-gateway-role \
  --policy-name memory-connector-access \
  --policy-document file://memory-connector-access.json

Gatewayの作成

Gateway本体を作成します。ここではprotocolTypeを指定しません。MCPプロトコルタイプで作成したGatewayにはHTTPターゲットを追加できず、次のエラーになるためです。

MCPプロトコルタイプのGatewayに追加した場合
HTTP target configuration is not supported for gateways with MCP protocol type.
Provide an MCP-compatible target configuration and retry the request.
create_gateway.py
import boto3

client = boto3.client("bedrock-agentcore-control", region_name="us-east-1")

response = client.create_gateway(
    name="memory-gateway-blog",
    roleArn="arn:aws:iam::<アカウントID>:role/memory-gateway-role",
    authorizerType="AWS_IAM",
)
print(response["gatewayId"], response["status"])
設定項目 設定値 説明
roleArn 作成した実行ロールのARN GatewayがMemoryへアクセスする際に使うロール
authorizerType AWS_IAM インバウンド認証をIAM(SigV4署名)にする
protocolType 指定しない 指定するとMCP専用になりHTTPターゲットを追加できないため

しばらく待つとステータスがREADYになり、gatewayUrlが発行されます。

ターゲットの作成

権限を追加したらターゲットを作成します。credentialProviderConfigurationsでアウトバウンド認証(GatewayからMemory側へアクセスするときの認証)の指定が必須で、今回はGatewayの実行ロールを使う GATEWAY_IAM_ROLE にします。

create_target.py
import boto3

client = boto3.client("bedrock-agentcore-control", region_name="us-east-1")

response = client.create_gateway_target(
    gatewayIdentifier="<GatewayのID>",
    name="memory-connector",
    targetConfiguration={
        "http": {
            "connector": {
                "source": {"connectorId": "agentcore-memory"},
                "parameters": {"memoryId": "<MemoryのID>"},
            }
        }
    },
    credentialProviderConfigurations=[
        {"credentialProviderType": "GATEWAY_IAM_ROLE"}
    ],
)
print(response["targetId"], response["status"])
設定項目 設定値 説明
connectorId agentcore-memory 使用するマネージドコネクタの識別子
parameters.memoryId MemoryのID このターゲットからアクセスを許可するMemoryを固定
credentialProviderType GATEWAY_IAM_ROLE Gateway実行ロールでSigV4署名してMemoryへアクセス

しばらく待つとステータスがREADYになりました。これで作成は完了です。

補足

今回の構成(AWS_IAMインバウンド + GATEWAY_IAM_ROLEアウトバウンド)では、Memoryが認可する主体は呼び出し元本人ではなくGateway実行ロールです。そのため呼び出し元個人を対象にしたIAMポリシー(特定actorIdのDenyなど)はMemory側では適用されず、呼び出し元単位の制御は後述のCedarポリシーで行います。

呼び出し元のIAMアイデンティティをそのままMemoryへ転送したい場合は、アウトバウンドを CALLER_IAM_CREDENTIALS と言った設定もあるのでそちらを利用します。

動作確認

Gateway経由でMemoryを読み取る

HTTPターゲットはパスベースルーティング(URLのパスで宛先ターゲットを切り替える方式)で呼び出します。URLは次の形式です。

URL形式
https://{gatewayId}.gateway.bedrock-agentcore.{region}.amazonaws.com/{targetName}/{path}

pathの部分には、Memoryコネクタが対応するデータプレーン操作(先ほどの12オペレーション)のパスを指定します。

たとえばアクター(Memory上の利用者単位)の一覧を取得するListActorsなら /memories/{memoryId}/actors です。Gatewayのインバウンド認証(呼び出し元からGatewayへの認証)はAWS_IAMなので、SigV4署名を付けてリクエストします。

invoke_memory.py
import json
import urllib.request

import boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest

creds = boto3.Session().get_credentials().get_frozen_credentials()
gateway_url = "https://<GatewayのID>.gateway.bedrock-agentcore.us-east-1.amazonaws.com"
memory_id = "<MemoryのID>"

def call(path: str, body: dict) -> None:
    url = gateway_url + path
    data = json.dumps(body).encode()
    request = AWSRequest(
        method="POST", url=url, data=data,
        headers={"Content-Type": "application/json"},
    )
    SigV4Auth(creds, "bedrock-agentcore", "us-east-1").add_auth(request)
    req = urllib.request.Request(
        url, data=data, headers=dict(request.headers), method="POST"
    )
    with urllib.request.urlopen(req) as resp:
        print(resp.status, resp.read().decode())

call(f"/memory-connector/memories/{memory_id}/actors", {"maxResults": 10})

署名のサービス名は bedrock-agentcore を指定します。リポジトリのスクリプトでは次のコマンドで実行できます。

実行コマンド
uv run scripts/invoke_memory.py --gateway-id <GatewayのID> --memory-id <MemoryのID>

実行すると無事レスポンスが返ってきました。

実行結果
200 {"actorSummaries":[{"actorId":"travel-user"}]}

Gateway経由でMemoryのデータプレーンにアクセスできていますね!

クライアント側はMemoryのエンドポイントを知る必要がなく、Gatewayだけを見ればよい構成になります。

アクセス制御を確認する

アクセス制御も確認してみます。ターゲットのparametersで固定したMemory以外のIDをパスに指定してみます。

別のMemoryへのアクセス
call("/memory-connector/memories/<別のMemoryのID>/actors", {"maxResults": 10})
実行結果
400 {"success":false,"error":"Request path does not match the target's configured resource"}

こちらはブロックされました!パスのmemoryIdとターゲット設定のparametersが突き合わされ、一致しない場合は400が返却されます。

同じアカウントに他のMemoryがあっても、このターゲットからは触れない作りになっていますね。

書き込みも試す

読み取りだけでなく書き込み(CreateEvent)も試してみます。パスは /memories/{memoryId}/events です。SDK経由のCreateEventでは自動設定されるclientToken(同じリクエストの重複実行を防ぐための一意なトークン)が、コネクタ経由で生のHTTPリクエストを送る今回の方法では明示的な指定が必要でした。

create_event.py
import time
import uuid

call(
    f"/memory-connector/memories/{memory_id}/events",
    {
        "clientToken": str(uuid.uuid4()),
        "actorId": "blog-test-user",
        "sessionId": "blog-test-session",
        "eventTimestamp": int(time.time()),
        "payload": [
            {
                "conversational": {
                    "content": {"text": "Gateway経由で書き込むテストメッセージです"},
                    "role": "USER",
                }
            }
        ],
    },
)
実行結果
201 {"event":{"actorId":"blog-test-user","branch":{"name":"main"},
"eventId":"0000001786189541000#15c48ea0", ... }}

ListEventsで読み込むと、書き込んだイベントがそのまま取得できました。

読み込み結果
200 {"events":[{"actorId":"blog-test-user", ...
"payload":[{"conversational":{"content":{"text":"Gateway経由で書き込むテストメッセージです"},
"role":"USER"}}],"sessionId":"blog-test-session"}]}

無事、読み書き両方がGateway経由でできました!

ユースケースを試す

基本動作が確認できたので、実際に使いそうなシーンをイメージしたシナリオを3つ試してみます。

管理画面から会話履歴を閲覧する

チャットアプリで過去の会話履歴を表示したいケースはよくあります。ただフロントエンドや管理画面にMemoryのIAM権限を直接渡すわけにはいかないので、これまでは間に履歴取得用のAPIを自作して挟んだりしました。そこをMemoryコネクタターゲットを使って、閲覧側はGatewayにリクエストするだけにできないか考えてみました。

今回はカスタマーサポートを想定して、エージェント役がSDKで会話をMemoryに書き込み、担当者の管理画面役がGateway経由で履歴を閲覧する構成にしました。リポジトリの uv run scripts/viewer_demo.py で試せます。

viewer_demo.py
# 管理画面役: Gateway経由でMemoryの会話履歴を閲覧する(Memoryへの直接権限は不要)
# call()はレスポンスのJSONを返すように変更したものを使います
def call(path: str, body: dict) -> dict:
    url = gateway_url + path
    data = json.dumps(body).encode()
    request = AWSRequest(
        method="POST", url=url, data=data,
        headers={"Content-Type": "application/json"},
    )
    SigV4Auth(creds, "bedrock-agentcore", "us-east-1").add_auth(request)
    req = urllib.request.Request(
        url, data=data, headers=dict(request.headers), method="POST"
    )
    with urllib.request.urlopen(req) as resp:
        return json.loads(resp.read())

actor_id = "customer-001"

# 1. 顧客のセッション一覧
sessions = call(
    f"/memory-connector/memories/{memory_id}/actor/{actor_id}/sessions",
    {"maxResults": 10},
)
print(f"=== {actor_id} のセッション一覧 ===")
for s in sessions["sessionSummaries"]:
    print(f"  {s['sessionId']}")

# 2. セッションを選んで会話履歴を表示
session_id = sessions["sessionSummaries"][0]["sessionId"]
events = call(
    f"/memory-connector/memories/{memory_id}/actor/{actor_id}/sessions/{session_id}",
    {"maxResults": 50},
)
print(f"\n=== 会話履歴: {session_id} ===")
for e in sorted(events["events"], key=lambda x: x["eventTimestamp"]):
    for p in e["payload"]:
        conv = p.get("conversational")
        if conv:
            speaker = "顧客" if conv["role"] == "USER" else "エージェント"
            print(f"  [{speaker}] {conv['content']['text']}")
実行結果
=== customer-001 のセッション一覧 ===
  support-session-042

=== 会話履歴: support-session-042 ===
  [顧客] 注文した商品がまだ届きません。注文番号はORD-1234です。
  [エージェント] ご不便をおかけしております。ORD-1234の配送状況を確認しますね。現在配送センターを出発済みで、明日午前中に到着予定です。
  [顧客] ありがとうございます。ちなみに置き配に変更できますか?
  [エージェント] 承知しました。置き配(玄関前)に変更いたしました。到着時に通知が届きます。

セッション一覧から会話履歴まで、Gatewayへのリクエストで取得できました。
Gatewayでプロキシすれば、フロントエンドが保持するトークンでMemoryのデータも取得できそうです。

ターゲットごとにMemoryを固定する

次はテナントごとにMemoryを分けるSaaS構成を想定します。テナントBのMemoryを固定した2つ目のターゲットを同じGatewayに追加して、ターゲットとMemoryの対応が固定されることを確認します。

テナントB用ターゲットの追加
response = client.create_gateway_target(
    gatewayIdentifier="<GatewayのID>",
    name="tenant-b-memory",
    targetConfiguration={
        "http": {
            "connector": {
                "source": {"connectorId": "agentcore-memory"},
                "parameters": {"memoryId": "<テナントBのMemoryのID>"},
            }
        }
    },
    credentialProviderConfigurations=[
        {"credentialProviderType": "GATEWAY_IAM_ROLE"}
    ],
)

ターゲットとMemoryの組み合わせを総当たりでリクエストしてみました。

呼び出し 結果
A用ターゲット × AのMemory 200
A用ターゲット × BのMemory 400 (Request path does not match)
B用ターゲット × BのMemory 200
B用ターゲット × AのMemory 400 (Request path does not match)

対応する組み合わせだけが通り、パスを入れ替えたリクエストは双方向とも400でブロックされました。各ターゲットからアクセスできるMemoryを1つに固定できています。

ただし、ここで保証されるのはターゲットとMemoryの対応までです。両方のターゲットを呼べる利用者はB用ターゲット経由でBのMemoryに届くため、テナント分離にはこの後のJWTセクションのように、利用者とターゲットを結び付ける認可をする必要があります。

Cedarポリシーで書き込みを禁止する

最後にAgentCore Policyとの組み合わせです。

先ほどの管理画面のシナリオで、閲覧ユーザーは履歴を読めるが書き込めない、という制御をCedarポリシーで実現してみます。

GatewayにPolicy EngineをENFORCEモードで紐付け、違反リクエストを実際にブロックする構成にします。前提として、Cedarはデフォルト拒否です。どの許可ルール(permit)にも一致しないリクエストはすべて拒否されるため、まずベースとなる許可ルールを作成しておきます。principal種別と対象Gatewayを絞った内容です。

ベースの許可ポリシー
statement = (
    'permit (principal is AgentCore::IamEntity, action, '
    'resource == AgentCore::Gateway::"<GatewayのARN>");'
)

client.create_policy(
    policyEngineId="<Policy EngineのID>",
    name="MemoryGatewayBase",
    definition={"policy": {"statement": statement}},
    validationMode="IGNORE_ALL_FINDINGS",
)

ポリシーは作成時に内容の事前チェックが走り、指摘が1件でもあるとデフォルトでは作成に失敗します。

今回の許可ルールはスコープを絞っても「対象のアクションを全部許可することになる」という指摘(Overly Permissive)が出て、失敗してしまいました。ただ、今回意図した設定そのものではあるので、内容を確認したうえで、チェックの指摘を無視する validationMode="IGNORE_ALL_FINDINGS" を指定して作成しています。

そのうえで、書き込みに対する禁止ルール(forbid)を作成しました。アクションは <ターゲット名>___<メソッド>:<パス> の形式で、Memoryコネクタの組み込みスキーマからパスがそのままアクション名として登録されています。

create_policy.py
statement = (
    'forbid (principal is AgentCore::IamEntity, '
    'action == AgentCore::Action::"memory-connector___POST:/memories/{memoryId}/events", '
    'resource == AgentCore::Gateway::"<GatewayのARN>") '
    'when { principal.id like "*<閲覧ユーザーのロール名>*" };'
)

client.create_policy(
    policyEngineId="<Policy EngineのID>",
    name="DenyMemoryWriteForViewer",
    definition={"policy": {"statement": statement}},
)

ポリシーがACTIVEになった状態で、閲覧ユーザーとして読み取りと書き込みを実行します。

実行結果
読み取り(ListEvents) -> 200 {"events":[...]}
書き込み(CreateEvent) -> 403 {"success":false,"error":"Request Denied: Gateway Target
request not allowed due to policy enforcement
[Policy evaluation denied due to DenyMemoryWriteForViewer-u1nslq9nra]"}

読み取りは通り、書き込みだけが403でブロックされました。リソース固定に加えて、どの操作を誰に許すかまでGateway側に寄せられます。守れるのはGatewayを通るリクエストだけなので、直接アクセスの迂回は後述のリソースベースポリシーで防御します。

principal.id はセッション名を含まない arn:aws:sts::<アカウントID>:assumed-role/<ロール名> 形式です。セッション名まで含むパターンではマッチしなかったため、"*<ロール名>*" に変更して適用しています。

JWT認証でテナント・ユーザー単位の認可制御まで広げる

ここまでのGatewayはインバウンド認証がAWS_IAM(IAM認証)でした。

ただ、フロントエンドや外部クライアントに公開するならJWT認証を使用するケースがあると思います。ということで、Cognitoのユーザートークンで呼び出して、トークンのクレーム(トークンに含まれるユーザーの属性情報)でテナント分離とロール制御ができるか確認してみます。

構成はCognitoユーザープールにテナントIDとロールをカスタム属性として持たせ、CUSTOM_JWT認証のGatewayにテナントA/B用の2つのMemoryコネクタターゲットとPolicy Engineを紐づけます。

Cognito側の準備

ユーザープールを作成し、カスタム属性 tenant_id / role を追加します。ユーザーはテナントAの閲覧ユーザーと、テナントBの管理者ユーザーの2人を作りました。

ユーザープールとカスタム属性の作成
aws cognito-idp create-user-pool --pool-name memory-gateway-jwt-pool

aws cognito-idp add-custom-attributes --user-pool-id <プールID> \
  --custom-attributes \
  Name=tenant_id,AttributeDataType=String,Mutable=true \
  Name=role,AttributeDataType=String,Mutable=true

aws cognito-idp create-user-pool-client --user-pool-id <プールID> \
  --client-name user-client \
  --explicit-auth-flows ALLOW_USER_PASSWORD_AUTH ALLOW_REFRESH_TOKEN_AUTH
テナントA閲覧ユーザーの作成(テナントB管理者も同様)
aws cognito-idp admin-create-user --user-pool-id <プールID> \
  --username tenant-a-user \
  --user-attributes Name=custom:tenant_id,Value=tenant-a Name=custom:role,Value=viewer \
  --message-action SUPPRESS
aws cognito-idp admin-set-user-password --user-pool-id <プールID> \
  --username tenant-a-user --password '<パスワード>' --permanent

JWT認証のGatewayを作成する

authorizerTypeを CUSTOM_JWT にしてGatewayを作成します。

create_gateway_jwt.py
response = client.create_gateway(
    name="memory-gateway-jwt",
    roleArn="arn:aws:iam::<アカウントID>:role/memory-gateway-role",
    authorizerType="CUSTOM_JWT",
    authorizerConfiguration={
        "customJWTAuthorizer": {
            "discoveryUrl": "https://cognito-idp.us-east-1.amazonaws.com/<プールID>/.well-known/openid-configuration",
            "allowedAudience": ["<アプリクライアントID>"],
        }
    },
)

トークンの種類は注意で、カスタム属性はCognitoのIDトークンにしか含まれず、当初アクセストークン(allowedClientsで検証)を使ったらポリシーで参照するクレームが空になります。allowedAudienceに変えてIDトークンを渡す構成にしています。

作成後、Memoryコネクタターゲットをテナントごとに2つ(memory-connector と tenant-b-memory)追加しておきます。作り方は前のセクションと同じです。

トークンで呼び出す

ユーザーとしてログインし、IDトークンをBearerヘッダーに付けて呼び出します。リポジトリでは uv run scripts/invoke_with_jwt.py が対応します。

invoke_with_jwt.py
import boto3

cognito = boto3.client("cognito-idp", region_name="us-east-1")
result = cognito.initiate_auth(
    ClientId="<アプリクライアントID>",
    AuthFlow="USER_PASSWORD_AUTH",
    AuthParameters={"USERNAME": "tenant-a-user", "PASSWORD": "<パスワード>"},
)
id_token = result["AuthenticationResult"]["IdToken"]

# SigV4署名の代わりにBearerヘッダーを付けるだけ
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {id_token}"}
実行結果
有効なIDトークン -> 200 {"actorSummaries":[...]}
不正なトークン -> 401 {"success":false,"error":"Invalid Bearer token"}

クライアント側はAWSの認証情報を一切持たず、Cognitoのログインだけで呼び出せました!
なお、ブラウザから直接呼び出す場合はCORS(ブラウザのクロスオリジン制約)の検証が別途必要となります。

Cedarでテナント分離とロール制御

トークンのクレームを使った認可制御も確認してみます。

Cedarポリシーでは principal.getTag でJWTのクレームを参照できます。IAMのときと同様にベースの許可ルール(principalは AgentCore::OAuthUser に変更)を作成したうえで、テナント制御2本と、書き込みは管理者のみという禁止ルールを設定しました。

テナント制御には action in AgentCore::Action::"<ターゲット名>" というAction Groupを使います。ターゲットに属する全アクション(ListActors、ListSessions、CreateEventなど)がまとめて対象になるので、アクションを1つずつ列挙しなくてもターゲット単位でテナント条件を適用できます。A/B両方のターゲットに対称に作成します。

テナント制御ポリシー(Aターゲット。Bターゲットも対称に作成)
statement = (
    'forbid (principal is AgentCore::OAuthUser, '
    'action in AgentCore::Action::"memory-connector", '
    'resource == AgentCore::Gateway::"<GatewayのARN>") '
    'unless { principal.hasTag("custom:tenant_id") '
    '&& principal.getTag("custom:tenant_id") == "tenant-a" };'
)

書き込み制御は両ターゲットのCreateEventをリストで指定します。

書き込みは管理者のみポリシー
statement = (
    'forbid (principal is AgentCore::OAuthUser, '
    'action in [AgentCore::Action::"memory-connector___POST:/memories/{memoryId}/events", '
    'AgentCore::Action::"tenant-b-memory___POST:/memories/{memoryId}/events"], '
    'resource == AgentCore::Gateway::"<GatewayのARN>") '
    'unless { principal.hasTag("custom:role") '
    '&& principal.getTag("custom:role") == "admin" };'
)

クレームの中身は実行時まで分からないため、こちらは事前チェックが「全リクエストを拒否しうる」という指摘(Overly Restrictive)を出して作成に失敗します。
IAMのときと同じく、指摘の内容が意図どおりであることを確認して validationMode="IGNORE_ALL_FINDINGS" で作成しました。

Policy Engineのモードは、記録だけでブロックしないLOG_ONLYと、実際にブロックするENFORCEを切り替えられます。挙動を比較してみました。

モードごとの挙動(ポリシー違反リクエストの場合)
LOG_ONLY -> 200 (通るが評価結果はログに記録される)
ENFORCE  -> 403 (ブロックされる)

余談ですが、まずLOG_ONLYで観察してからENFORCEに上げる流れが安全ですね。
ENFORCEに切り替えて、2人のユーザーで総当たりした結果は下記の通りでした。

呼び出し 結果
テナントA閲覧ユーザー × Aターゲット読み取り(ListActors) 200
テナントA閲覧ユーザー × Bターゲット読み取り(ListActors) 403 (Bテナント制御)
テナントA閲覧ユーザー × Bターゲット読み取り(ListSessions) 403 (Bテナント制御)
テナントB管理者 × Bターゲット読み取り(ListActors) 200
テナントB管理者 × Aターゲット読み取り(ListActors) 403 (Aテナント制御)
テナントA閲覧ユーザー × Aターゲット書き込み(CreateEvent) 403 (管理者のみ)
テナントB管理者 × Bターゲット書き込み(CreateEvent) 201
テナントB管理者 × Aターゲット書き込み(CreateEvent) 403 (Aテナント制御)

自テナントのターゲットだけが有効で、他テナントへのアクセスは読み書きとも双方向で403になりました。
Action GroupのおかげでListSessionsのような個別に指定していないアクションもブロックされています。

JWTのクレームがそのままCedarのprincipalタグとして参照でき、誰がどのテナントのMemoryにどの操作をできるかを、Gateway経由のリクエストについてはGateway層で制御できました。アプリケーション側に認可ロジックを書かずにGatewayに寄せることもできそうですね。

Action Groupなどポリシーの対象指定の書き方は公式ドキュメントにまとまっているので必要に応じて参考にしましょう!

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy-scope.html

ただし、ここまでで防げているのはテナントとロール単位までとなります。

同一テナントの中では、利用者Aが利用者BのactorIdを指定して読むことまでは止められません。そこで、公式ドキュメントが本命として挙げている、リクエストのactorIdとJWTのsubクレームを突き合わせるユーザー単位の分離パターンも試してみます。context.input でリクエストのパスパラメータやボディの値を参照できるので、actorIdがトークンのsubと一致しない限りListEventsを拒否するポリシーを追加しました。

自分のアクター以外を拒否するポリシー
statement = (
    'forbid (principal is AgentCore::OAuthUser, '
    'action == AgentCore::Action::"memory-connector___POST:/memories/{memoryId}/actor/{actorId}/sessions/{sessionId}", '
    'resource == AgentCore::Gateway::"<GatewayのARN>") '
    'unless { principal.hasTag("sub") && context has input && context.input has actorId '
    '&& context.input.actorId == principal.getTag("sub") };'
)

context.input のフィールドは操作によって存在しないことがあるため、公式ドキュメントの案内どおり has でガードしてから参照しています。テナントAのユーザーで、自分のアクターと他人のアクターにアクセスした結果です。

実行結果
自分のactorId(=sub)のListEvents -> 200 {"events":[]}
他人のactorId(customer-001)のListEvents -> 403 (自分のアクター以外を拒否するポリシー)

ListEventsについて、各ユーザーが自分のアクターのイベントにしか参照できないことを確認できました!

運用ではGetEventやListSessionsなどactorIdを持つ操作にも同じ条件を適用し、actorIdを持たないRetrieveMemoryRecordsはnamespacePathとクレームの比較で分離します。

移行パターンも含めたMemory向けのポリシー例は公式ドキュメントに記載があります!

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory-gateway-fgac-policy-examples.html

直接アクセスを塞いでGateway経由に限定する

ここまでの制御はすべてGatewayを通るリクエストを対象にしています。

逆に言うと、MemoryへのIAM権限を持つ主体がデータプレーンを直接呼ぶと、Gatewayのポリシーを一切経由せずにアクセスできてしまいます。公式ドキュメントでは、Memoryのリソースベースポリシーでアクセス元をGatewayに限定する方法が案内されています。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory-gateway-restrict-access.html

同一アカウントでIAM権限を持つ主体はAllowがなくても直接呼べるため、塞ぐには明示的なDenyが必要です。

また条件キーは、公式ドキュメントにはGatewayが aws:SourceArn を記載するとありますが、2026年8月10日と12日にus-east-1で検証したところ、いずれもGATEWAY_IAM_ROLE経由のリクエストではSourceArn条件が成立しませんでした。私の環境や、公式記載との差分や一時的な反映状況の可能性があるため、実際に使用する際は再確認してください・・・!

参考までに、SourceArnで書く場合は下記のように、Gateway経由(aws:SourceArnがGatewayのARNと一致)以外を拒否する形になります。特定のGatewayだけに絞れるので、機能するならこちらのほうがきれいです。

SourceArnで書く場合の例(検証時点では成立せず)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnlessFromGateway",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "bedrock-agentcore:ListActors",
      "Resource": "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:memory/<MemoryのID>",
      "Condition": {
        "ArnNotEquals": {
          "aws:SourceArn": "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:gateway/<GatewayのID>"
        }
      }
    }
  ]
}

検証時点ではこのポリシーだとGateway経由のリクエストまで拒否されてしまったため、今回は公式がGATEWAY_IAM_ROLEモード向けに案内している aws:PrincipalArn を実行ロールに限定する方式で試します。

今回は挙動確認のため、ListActorsだけを直接アクセス禁止の対象にします。

Gateway実行ロール以外を拒否するリソースベースポリシー
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyExceptGatewayRole",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "bedrock-agentcore:ListActors",
      "Resource": "arn:aws:bedrock-agentcore:us-east-1:<アカウントID>:memory/<MemoryのID>",
      "Condition": {
        "StringNotLike": {"aws:PrincipalArn": "*role/<Gateway実行ロール名>*"}
      }
    }
  ]
}

PutResourcePolicyでMemoryに適用して、直接アクセスとGateway経由のそれぞれで同じListActorsを実行してみました。

実行結果
直接ListActors        -> AccessDeniedException (User ... is not authorized)
Gateway経由ListActors -> 200 {"actorSummaries":[...]}

同じ呼び出し元・同じ操作でも、Gatewayを通ったリクエストだけ疎通できました!

ListActorsについて、直接アクセスを拒否してGateway経由だけを許可できました。直接アクセスはリソースベースポリシーで拒否し、Gatewayを通ったアクセスはCedarで認可する、という組み合わせです。

なお、このDenyは実行ロールを共有するすべてのGatewayに適用されるので注意してください。

どういうときに使うのか考えてみる

個人的に考えてみました。クライアントごとにMemoryのIAM権限を付与しなくてよくなるのが嬉しいのかなと思いました。

履歴を見たい管理画面が増えても、テナントが増えても、渡すのはGatewayへのアクセス権限だけです。JWT認証にすればクライアントはCognitoのログインだけで済み、AWSの認証情報を配る必要がありません。誰がどのMemoryにどの操作をできるかもクレームとCedarでGateway側に寄せられるので、アプリケーションに認可ロジックを書かずに済みます。インターセプターなどのGatewayの統制機能が利用できるのも嬉しいですよね。

なお、直接アクセスの迂回まで塞ぐには、前述のとおりリソースベースポリシーを組み合わせます。

Memoryを外に公開する方法としては、以前検証したリソースベースポリシーによるクロスアカウントアクセスもあります。あちらはIAMの権限を相手アカウントに開放するアプローチなので、SDKからそのまま使えて手軽な一方、IAMを持つ相手にしか公開できず、統制もIAMポリシー頼みになります。Gateway経由の今回の方式は、IAMからJWTへの認証方式の変換や、クレームベースの認可まで含めて統制したい場合に選ぶことになりそうです。

https://dev.classmethod.jp/articles/agentcore-memory-cross-account-access/

InferenceターゲットでLLM呼び出しをGatewayに集約、Agent Targetでエージェント間通信をGatewayに集約ときて、今回のMemoryコネクタでメモリアクセスもGatewayに集約となり、エージェントを取り巻くトラフィックを1か所で管理する方向性が一段と進んだ印象ですね・・・!!!

おわりに

今回の検証で動作を確認できたHTTPコネクタは agentcore-memory だけでしたが、コネクタという仕組み上、今後新しいサービスが追加されていく気がしています!
AgentCore Gatewayでできることがどんどん増えてきたので、一度立ち止まり、本番利用でどう使うのか整理してみたいですね!

本記事が少しでも参考になりましたら幸いです。
最後までご覧いただきありがとうございました!

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事