
A connector type was added to the HTTP target of AgentCore Gateway, so I tried connecting it to AgentCore Memory
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.
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.
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.
{
"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.
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.
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.
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.
{
"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>"
}
]
}
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.
HTTP target configuration is not supported for gateways with MCP protocol type.
Provide an MCP-compatible target configuration and retry the request.
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.
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.
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.
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.
uv run scripts/invoke_memory.py --gateway-id <Gateway ID> --memory-id <Memory ID>
Upon execution, a response was successfully returned.
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.
call("/memory-connector/memories/<different Memory ID>/actors", {"maxResults": 10})
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.
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",
}
}
],
},
)
201 {"event":{"actorId":"blog-test-user","branch":{"name":"main"},
"eventId":"0000001786189541000#15c48ea0", ... }}
When reading with ListEvents, the written event was retrieved as-is.
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.
# 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']}")
=== 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.
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.
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.
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.
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.
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
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.
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.
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}"}
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.
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.
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.
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!
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.
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.
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!
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.
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.
{
"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.
{
"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:
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.
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!
