A connector type was added to the HTTP target of AgentCore Gateway, so I tried connecting it to AgentCore Memory

A connector type was added to the HTTP target of AgentCore Gateway, so I tried connecting it to AgentCore Memory

I tried out the newly added HTTP target connector type in Amazon Bedrock AgentCore Gateway!
2026.08.12

This page has been translated by machine translation. View original

Introduction

Hello, I'm Kamino from the Consulting Department, and I love supermarkets.

In the API update on August 5, 2026, there was an interesting change to 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.

It appears that a new type called connector has been added to Gateway's HTTP targets, enabling routing through managed connectors (built-in connection destinations provided by AWS) and fine-grained access control. That might not quite click yet...

In conclusion, this was a feature that allows AgentCore Memory to be exposed through a Gateway! We'll verify everything including authorization control with Cedar, based on the official documentation.

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

I've been writing articles about Gateway targets up until now, and this update felt like a continuation of those.

Update Overview

Gateway's HTTP targets previously had two types.

Type Description
agentcoreRuntime Routes to agents in AgentCore Runtime
passthrough Forwards directly to external HTTP endpoints

With this update, connector has been added as a third type!

Type Description
connector Routes through managed connectors

The target configuration is a simple structure that just passes a connectorId and parameters to fix the resource.

Target configuration structure
{
  "http": {
    "connector": {
      "source": {
        "connectorId": "agentcore-memory"
      },
      "parameters": {
        "memoryId": "<ID of the Memory you want to expose>"
      }
    }
  }
}

The official API Reference already has the HttpConnectorTargetConfiguration page published, and the description for parameters includes the following:

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

This means it validates by matching the resource specified in parameters against the request path at runtime. However, this is a mechanism for fixing the target connection destination, and authorization for who can access which actors and which operations is handled by Cedar policies in the Policy Engine described later.

There were already connectors for MCP targets like Web Search and connector types for Inference targets (like Bedrock Mantle), but that mechanism has now expanded to HTTP targets as well...!

Prerequisites

Here is the environment used during verification.

Item Details
Region us-east-1
boto3 / botocore botocore 1.43.67 (supports the new API from 1.43.65 onwards)
Gateway Created fresh in this article (inbound authentication: AWS_IAM (IAM authentication))
Memory Pre-existing AgentCore Memory

Since the new API is included in botocore 1.43.65 (released August 5, 2026) and later, please update if you have an older version!

The code used in this article is compiled in the repository below. Dependencies are fixed with uv.lock as a uv project, and cloning and running uv run will automatically resolve dependencies on the first run. Since the code in the article is excerpted, it's recommended to clone the repository if you want to reproduce it locally.

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

Execution example
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::<account ID>:role/memory-gateway-role

Memory Connector Overview

According to the official documentation, what's provided as an HTTP connector is the AgentCore Memory connector, with a connectorId of agentcore-memory. By fixing the memoryID in the target, the connector takes care of both the wiring to Memory's data plane and providing the API schema.

It supports the following 12 Memory data plane operations, each of which is exposed as a Cedar policy action.

I think it's easiest to understand this as: these APIs are now accessible through the Gateway.

Category Operations
Events CreateEvent / GetEvent / ListEvents / DeleteEvent / ListSessions / ListActors
Long-term memory RetrieveMemoryRecords / ListMemoryRecords / GetMemoryRecord / DeleteMemoryRecord
Extraction jobs ListMemoryExtractionJobs / StartMemoryExtractionJob

Since this might be hard to understand, let's go ahead and create a Gateway to try it out!

Creating a Memory Connector Target

Let's actually create it now. We'll create things in this order: the Gateway execution role, the Gateway itself, and the target.

Creating the Gateway Execution Role

First, create the Gateway execution role. The trust policy allows AssumeRole from the bedrock-agentcore service.

Creating the role
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": "<account ID>"}}
    }]
  }'

This role also needs permissions to access the target Memory.

Add the following inline policy to the role.

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:<account ID>:memory/<Memory ID>"
    }
  ]
}
Granting Memory access permissions
aws iam put-role-policy \
  --role-name memory-gateway-role \
  --policy-name memory-connector-access \
  --policy-document file://memory-connector-access.json

Creating the Gateway

Create the Gateway itself. We won't specify protocolType here. This is because HTTP targets cannot be added to a Gateway created with the MCP protocol type, resulting in the following error.

When adding to a Gateway with MCP protocol type
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::<account ID>:role/memory-gateway-role",
    authorizerType="AWS_IAM",
)
print(response["gatewayId"], response["status"])
Setting Value Description
roleArn ARN of the created execution role The role used by the Gateway when accessing Memory
authorizerType AWS_IAM Sets inbound authentication to IAM (SigV4 signature)
protocolType Not specified Specifying this makes it MCP-only and HTTP targets cannot be added

After waiting a while, the status becomes READY and a gatewayUrl is issued.

Creating the Target

Once permissions are added, create the target. credentialProviderConfigurations requires specifying the outbound authentication (authentication when the Gateway accesses Memory), and we'll use GATEWAY_IAM_ROLE which uses the Gateway's execution 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"])
Setting Value Description
connectorId agentcore-memory Identifier of the managed connector to use
parameters.memoryId Memory ID Fixes the Memory accessible from this target
credentialProviderType GATEWAY_IAM_ROLE Accesses Memory with SigV4 signature using the Gateway execution role

After waiting a while, the status became READY. Creation is now complete.

Supplementary Notes

In this configuration (AWS_IAM inbound + GATEWAY_IAM_ROLE outbound), the principal that Memory authorizes is the Gateway execution role, not the caller themselves. Therefore, IAM policies targeting individual callers (such as Deny for a specific actorId) are not applied on the Memory side, and per-caller control is handled by Cedar policies described later.

If you want to forward the caller's IAM identity directly to Memory, there is also a setting called CALLER_IAM_CREDENTIALS for the outbound configuration.

Verification

Reading Memory through the Gateway

HTTP targets are called using path-based routing (a method that switches the destination target based on the URL path). The URL format is as follows.

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

For the path portion, you specify the path of the data plane operation (the 12 operations from earlier) supported by the Memory connector.

For example, for ListActors, which retrieves a list of actors (units per user on Memory), it's /memories/{memoryId}/actors. Since the Gateway's inbound authentication (authentication from callers to the Gateway) is AWS_IAM, requests are made with SigV4 signatures attached.

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})

Specify bedrock-agentcore as the service name for the signature. In the repository's scripts, you can run this with the following command.

Execution command
uv run scripts/invoke_memory.py --gateway-id <Gateway ID> --memory-id <Memory ID>

Upon execution, a response was successfully returned.

Execution result
200 {"actorSummaries":[{"actorId":"travel-user"}]}

We can access Memory's data plane through the Gateway!

The client side doesn't need to know the Memory endpoint — it only needs to look at the Gateway.

Verifying Access Control

Let's also verify access control. We'll try specifying a Memory ID other than the one fixed in the target's parameters.

Accessing a different Memory
call("/memory-connector/memories/<different Memory ID>/actors", {"maxResults": 10})
Execution result
400 {"success":false,"error":"Request path does not match the target's configured resource"}

This was blocked! The memoryId in the path is matched against the parameters in the target configuration, and if they don't match, a 400 is returned.

Even if there are other Memories in the same account, this target is set up so they cannot be touched.

Testing Writes as Well

Let's try not just reading but also writing (CreateEvent). The path is /memories/{memoryId}/events. The clientToken (a unique token to prevent duplicate execution of the same request), which is automatically set when using the SDK's CreateEvent, needed to be explicitly specified with the raw HTTP request approach via the connector used this time.

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": "This is a test message written via Gateway"},
                    "role": "USER",
                }
            }
        ],
    },
)
Execution result
201 {"event":{"actorId":"blog-test-user","branch":{"name":"main"},
"eventId":"0000001786189541000#15c48ea0", ... }}

When reading with ListEvents, the written event was retrieved as-is.

Read result
200 {"events":[{"actorId":"blog-test-user", ...
"payload":[{"conversational":{"content":{"text":"This is a test message written via Gateway"},
"role":"USER"}}],"sessionId":"blog-test-session"}]}

Both reading and writing through the Gateway worked successfully!

Trying Use Cases

Now that basic operation has been confirmed, let's try three scenarios that simulate situations likely to come up in actual use.

Viewing Conversation History from an Admin Panel

There are common cases in chat apps where you want to display past conversation history. However, giving the frontend or admin panel direct IAM permissions to Memory is not feasible, so previously you had to build a custom history-retrieval API to sit in between. I considered whether using a Memory connector target could simplify the viewer side to just sending requests to the Gateway.

This time, imagining a customer support scenario, I set up a configuration where the agent role writes conversations to Memory using the SDK, and the admin panel role for the person in charge views the history through the Gateway. You can try this with uv run scripts/viewer_demo.py in the repository.

viewer_demo.py
# Admin panel role: View conversation history in Memory via Gateway (no direct Memory permissions needed)
# Uses a modified version of call() that returns the response 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. List of customer sessions
sessions = call(
    f"/memory-connector/memories/{memory_id}/actor/{actor_id}/sessions",
    {"maxResults": 10},
)
print(f"=== {actor_id}'s session list ===")
for s in sessions["sessionSummaries"]:
    print(f"  {s['sessionId']}")

# 2. Select a session and display conversation history
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=== Conversation history: {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 = "Customer" if conv["role"] == "USER" else "Agent"
            print(f"  [{speaker}] {conv['content']['text']}")
Execution result
=== customer-001's session list ===
  support-session-042

=== Conversation history: support-session-042 ===
  [Customer] My ordered item hasn't arrived yet. The order number is ORD-1234.
  [Agent] We apologize for the inconvenience. Let me check the shipping status for ORD-1234. It has already left the distribution center and is expected to arrive tomorrow morning.
  [Customer] Thank you. Can I also change it to unattended delivery?
  [Agent] Understood. I've changed it to unattended delivery (at the front door). You'll receive a notification when it arrives.

From the session list to the conversation history, everything was retrieved with requests to the Gateway.
By proxying through the Gateway, the frontend should be able to retrieve Memory data with the token it holds.

Fixing Memory Per Target

Next, let's simulate a SaaS configuration where Memory is separated per tenant. We'll add a second target with Memory fixed for tenant B to the same Gateway, and confirm that the correspondence between targets and Memory is fixed.

Adding a target for tenant B
response = client.create_gateway_target(
    gatewayIdentifier="<Gateway ID>",
    name="tenant-b-memory",
    targetConfiguration={
        "http": {
            "connector": {
                "source": {"connectorId": "agentcore-memory"},
                "parameters": {"memoryId": "<Tenant B's Memory ID>"},
            }
        }
    },
    credentialProviderConfigurations=[
        {"credentialProviderType": "GATEWAY_IAM_ROLE"}
    ],
)

We tried all combinations of targets and Memory in requests.

Call Result
Target for A × Memory A 200
Target for A × Memory B 400 (Request path does not match)
Target for B × Memory B 200
Target for B × Memory A 400 (Request path does not match)

Only matching combinations went through, and requests with swapped paths were blocked with 400 in both directions. The Memory accessible from each target is fixed to one.

However, what's guaranteed here is only the correspondence between the target and Memory. A user who can call both targets can reach Memory B via the B target, so for tenant isolation, authorization that links users to targets is needed, as in the JWT section later.

Prohibiting Writes with Cedar Policies

Finally, let's combine this with AgentCore Policy.

In the admin panel scenario from earlier, we'll use Cedar policies to implement control where viewer users can read history but cannot write.

We'll attach a Policy Engine to the Gateway in ENFORCE mode to actually block violating requests. As a prerequisite, Cedar is deny-by-default. Any request that doesn't match any permit rule is denied, so first we create a base permit rule. The content limits the principal type and target Gateway.

Base permit policy
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",
)

When a policy is created, a pre-check of its content runs, and if there is even one finding, creation fails by default.

The permit rule here would get an "Overly Permissive" finding even when scoping it down — meaning "this permits all the target actions" — causing creation to fail. However, since this is exactly the configuration we intended, after reviewing the content, we specified validationMode="IGNORE_ALL_FINDINGS" to ignore the check findings and created it.

On top of that, we created a forbid rule for writes. Actions are in the format <target name>___<method>:<path>, and paths from the connector's built-in schema are registered directly as action names.

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 "*<viewer user role name>*" };'
)

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

With the policy in ACTIVE state, we execute both read and write as the viewer user.

Execution result
Read (ListEvents) -> 200 {"events":[...]}
Write (CreateEvent) -> 403 {"success":false,"error":"Request Denied: Gateway Target
request not allowed due to policy enforcement
[Policy evaluation denied due to DenyMemoryWriteForViewer-u1nslq9nra]"}

Reads went through, and only writes were blocked with 403. In addition to fixing the resource, who is allowed which operations can be handled on the Gateway side. Note that this only protects requests going through the Gateway, so direct access bypass should be defended against with resource-based policies described later.

The principal.id is in the format arn:aws:sts::<account ID>:assumed-role/<role name> without the session name. Since patterns including the session name didn't match, we changed to "*<role name>*" and applied it.

Extending to Tenant- and User-Level Authorization Control with JWT Authentication

Up until this point, the Gateway's inbound authentication was AWS_IAM (IAM authentication).

However, if you're exposing it to frontends or external clients, there are cases where you'd use JWT authentication. So let's verify whether we can use Cognito user tokens to call and achieve tenant isolation and role control based on token claims (user attribute information contained in the token).

The configuration has Cognito user pools holding tenant ID and role as custom attributes, with a CUSTOM_JWT authentication Gateway linked to two Memory connector targets for tenant A/B and a Policy Engine.

Preparing the Cognito Side

Create a user pool and add custom attributes tenant_id / role. We created two users: a viewer user for tenant A and an admin user for tenant B.

Creating a user pool and custom attributes
aws cognito-idp create-user-pool --pool-name memory-gateway-jwt-pool

aws cognito-idp add-custom-attributes --user-pool-id <pool 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 <pool ID> \
  --client-name user-client \
  --explicit-auth-flows ALLOW_USER_PASSWORD_AUTH ALLOW_REFRESH_TOKEN_AUTH
Creating a tenant A viewer user (same for tenant B admin)
aws cognito-idp admin-create-user --user-pool-id <pool 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 <pool ID> \
  --username tenant-a-user --password '<password>' --permanent

Creating a Gateway with JWT Authentication

Create a Gateway with authorizerType set to CUSTOM_JWT.

create_gateway_jwt.py
response = client.create_gateway(
    name="memory-gateway-jwt",
    roleArn="arn:aws:iam::<account ID>:role/memory-gateway-role",
    authorizerType="CUSTOM_JWT",
    authorizerConfiguration={
        "customJWTAuthorizer": {
            "discoveryUrl": "https://cognito-idp.us-east-1.amazonaws.com/<pool ID>/.well-known/openid-configuration",
            "allowedAudience": ["<app client ID>"],
        }
    },
)

Be careful about the type of token: custom attributes are only included in Cognito's ID token, and when I initially used an access token (verified with allowedClients), the claims referenced in the policy were empty. I changed to allowedAudience and configured it to pass the ID token instead.

After creation, add two Memory connector targets per tenant (memory-connector and tenant-b-memory). The creation method is the same as the previous section.

Calling with Tokens

Log in as a user and call with the ID token attached in the Bearer header. In the repository, uv run scripts/invoke_with_jwt.py handles this.

invoke_with_jwt.py
import boto3

cognito = boto3.client("cognito-idp", region_name="us-east-1")
result = cognito.initiate_auth(
    ClientId="<app client ID>",
    AuthFlow="USER_PASSWORD_AUTH",
    AuthParameters={"USERNAME": "tenant-a-user", "PASSWORD": "<password>"},
)
id_token = result["AuthenticationResult"]["IdToken"]

# Just attach the Bearer header instead of SigV4 signature
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {id_token}"}
Execution result
Valid ID token -> 200 {"actorSummaries":[...]}
Invalid token -> 401 {"success":false,"error":"Invalid Bearer token"}

The client side holds no AWS credentials at all and can call using only Cognito login!
Note that if calling directly from a browser, CORS (browser cross-origin restrictions) verification is separately required.

Tenant Isolation and Role Control with Cedar

Let's also verify authorization control using token claims.

In Cedar policies, you can reference JWT claims with principal.getTag. Just like with IAM, we created a base allow rule (with principal changed to AgentCore::OAuthUser), then set up two tenant control rules and a deny rule restricting writes to administrators only.

For tenant control, we use an Action Group with action in AgentCore::Action::"<target name>". Since all actions belonging to a target (ListActors, ListSessions, CreateEvent, etc.) are covered collectively, you can apply tenant conditions per target without enumerating each action individually. We create these symmetrically for both A and B targets.

Tenant control policy (A target. B target created symmetrically)
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" };'
)

The write control specifies CreateEvent for both targets as a list.

Write restricted to administrators only policy
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" };'
)

Since the contents of claims are unknown until runtime, the pre-check flags this as potentially denying all requests (Overly Restrictive), causing creation to fail.
Just as with IAM, after confirming that the flagged content was intentional, we created it with validationMode="IGNORE_ALL_FINDINGS".

The Policy Engine mode can be switched between LOG_ONLY, which only records without blocking, and ENFORCE, which actually blocks. We compared the behavior of each.

Behavior per mode (for policy-violating requests)
LOG_ONLY -> 200 (passes, but evaluation result is recorded in logs)
ENFORCE  -> 403 (blocked)

As a side note, it's safer to first observe in LOG_ONLY before switching to ENFORCE.
After switching to ENFORCE and running an exhaustive test with two users, the results were as follows.

Call Result
Tenant A viewer × Target A read (ListActors) 200
Tenant A viewer × Target B read (ListActors) 403 (Tenant B control)
Tenant A viewer × Target B read (ListSessions) 403 (Tenant B control)
Tenant B admin × Target B read (ListActors) 200
Tenant B admin × Target A read (ListActors) 403 (Tenant A control)
Tenant A viewer × Target A write (CreateEvent) 403 (Admin only)
Tenant B admin × Target B write (CreateEvent) 201
Tenant B admin × Target A write (CreateEvent) 403 (Tenant A control)

Only the user's own tenant target was accessible, and cross-tenant access resulted in 403 in both directions for both reads and writes.
Thanks to Action Groups, actions not individually specified, such as ListSessions, were also blocked.

JWT claims can be referenced directly as Cedar principal tags, allowing control over who can perform which operations on which tenant's Memory — at the Gateway layer for requests going through the Gateway. It seems feasible to centralize authorization logic in the Gateway rather than writing it in the application.

The official documentation summarizes how to write policy scope specifications such as Action Groups, so refer to it as needed!

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

However, what we've protected so far only covers the tenant and role level.

Within the same tenant, it's still possible for user A to read by specifying user B's actorId. So let's also try the user-level isolation pattern that the official documentation highlights as the primary approach — matching the request's actorId against the JWT's sub claim. Since you can reference path parameters and body values from requests via context.input, we added a policy that denies ListEvents unless the actorId matches the token's sub.

Policy to deny access to actors other than one's own
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") };'
)

Since fields in context.input may not exist depending on the operation, we guard with has before referencing them, as instructed in the official documentation. Here are the results when a Tenant A user accessed their own actor and another user's actor.

Execution results
ListEvents for own actorId (= sub)       -> 200 {"events":[]}
ListEvents for another's actorId (customer-001) -> 403 (policy denying actors other than one's own)

We confirmed that for ListEvents, each user can only access events for their own actor!

In production, apply the same condition to operations that carry an actorId, such as GetEvent and ListSessions, and isolate RetrieveMemoryRecords — which lacks an actorId — by comparing namespacePath with claims.

Policy examples for Memory, including migration patterns, are documented in the official documentation!

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

Blocking Direct Access to Restrict to Gateway Only

All the controls so far apply to requests going through the Gateway.

Conversely, a principal with IAM permissions to Memory can call the data plane directly, bypassing all Gateway policies entirely. The official documentation describes a method to restrict access sources to the Gateway using a Memory resource-based policy.

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

Since principals within the same account with IAM permissions can call directly even without an explicit Allow, an explicit Deny is required to block them.

Regarding condition keys, the official documentation states that the Gateway writes aws:SourceArn, but when we tested in us-east-1 on August 10 and 12, 2026, the SourceArn condition did not hold for requests via GATEWAY_IAM_ROLE in either case. This may be specific to my environment, a discrepancy with the official documentation, or a temporary propagation issue, so please verify again when actually using this!

For reference, when written with SourceArn, the form would be as follows — denying everything except requests via Gateway (where aws:SourceArn matches the Gateway ARN). This is cleaner if it works, as it can be restricted to a specific Gateway.

Example using SourceArn (did not hold at time of testing)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnlessFromGateway",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "bedrock-agentcore:ListActors",
      "Resource": "arn:aws:bedrock-agentcore:us-east-1:<account ID>:memory/<Memory ID>",
      "Condition": {
        "ArnNotEquals": {
          "aws:SourceArn": "arn:aws:bedrock-agentcore:us-east-1:<account ID>:gateway/<Gateway ID>"
        }
      }
    }
  ]
}

Since this policy also denied requests coming through the Gateway at the time of testing, we instead used the method recommended in the official documentation for GATEWAY_IAM_ROLE mode — restricting aws:PrincipalArn to the execution role.

For this test, we limit the direct access restriction to ListActors only.

Resource-based policy denying all except the Gateway execution role
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyExceptGatewayRole",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "bedrock-agentcore:ListActors",
      "Resource": "arn:aws:bedrock-agentcore:us-east-1:<account ID>:memory/<Memory ID>",
      "Condition": {
        "StringNotLike": {"aws:PrincipalArn": "*role/<Gateway execution role name>*"}
      }
    }
  ]
}

After applying this to Memory with PutResourcePolicy and running the same ListActors operation both directly and via the Gateway:

Execution results
Direct ListActors        -> AccessDeniedException (User ... is not authorized)
Gateway ListActors       -> 200 {"actorSummaries":[...]}

Even with the same caller and same operation, only the request through the Gateway succeeded!

For ListActors, we were able to deny direct access and allow only Gateway-routed access. The combination is: deny direct access with a resource-based policy, and authorize Gateway-routed access with Cedar.

Note that this Deny applies to all Gateways sharing the same execution role.

Thinking About When to Use This

Here are some personal thoughts. I think the key benefit is not having to grant Memory IAM permissions to each client.

Even as more admin dashboards wanting to view history are added, or as tenants increase, all you need to hand out is access permission to the Gateway. With JWT authentication, clients only need to log in via Cognito, eliminating the need to distribute AWS credentials. Since who can perform which operations on which Memory can be centralized in the Gateway using claims and Cedar, there's no need to write authorization logic in the application. It's also great to be able to leverage Gateway governance features like interceptors.

To also block bypassing via direct access, combine with a resource-based policy as described above.

As a way to expose Memory externally, there's also the cross-account access via resource-based policy that we tested previously. That approach opens up IAM permissions to the counterpart account — straightforward to use directly from the SDK, but limited to counterparts who have IAM, with governance relying solely on IAM policies. The Gateway-based approach we explored this time would be the choice when you want to handle both authentication method conversion from IAM to JWT and claim-based authorization under unified governance.

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

With Inference targets consolidating LLM calls into the Gateway, Agent Targets consolidating inter-agent communication into the Gateway, and now Memory connectors consolidating memory access into the Gateway as well, the direction of managing all traffic surrounding agents in one place seems to have taken a significant step forward...!!!

Closing

The only HTTP connector whose behavior we were able to confirm in this testing was agentcore-memory, but given that it's a connector-based mechanism, I have a feeling new services will be added in the future!
As the capabilities of AgentCore Gateway continue to expand, it might be worth pausing to think through how to use it in production!

I hope this article proves useful even in a small way.
Thank you very much for reading to the end!

Share this article

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