[Quick Tip] Cross-Account Access to Amazon Bedrock AgentCore Gateway with IAM Authentication

[Quick Tip] Cross-Account Access to Amazon Bedrock AgentCore Gateway with IAM Authentication

2026.06.01

This page has been translated by machine translation. View original

Introduction

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

Are you using Amazon Bedrock AgentCore Gateway?
It's a service that allows you to integrate Lambda functions, REST APIs, Remote MCP servers, and more as MCP endpoints.

Given Gateway's nature of being able to integrate multiple tools, don't you ever want to centrally place a Gateway in a specific account and call it from AgentCore Runtimes in other accounts? I certainly do. This kind of arrangement is desirable when providing a common set of tools in a multi-account environment.

When I tried to connect to a Gateway created with IAM authentication (SigV4) from a different account, I got a 403 Forbidden error. Hmm, maybe it doesn't support cross-account... Do I have to use JWT...? I thought, but reading the official documentation resolved it immediately.

Conclusion Up Front

To access a Gateway with IAM authentication from a cross-account setup, configure a resource-based policy on the Gateway side. The official documentation also states the following, confirming that cross-account connections are supported.

Resource-based policies in Amazon Bedrock AgentCore allow you to control which principals (AWS accounts, IAM users, or IAM roles) can invoke and manage your Amazon Bedrock AgentCore resources (currently supported for Runtime, Gateway and Memory).

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/resource-based-policies.html

You might be wondering what kind of policy to write here, but the policy to set on the Gateway looks like the following.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::<caller-account-id>:root" },
      "Action": "bedrock-agentcore:InvokeGateway",
      "Resource": "arn:aws:bedrock-agentcore:<region>:<gateway-owner-account-id>:gateway/<gateway-id>"
    }
  ]
}

For Principal, specify the ARN of the calling account; for Action, specify bedrock-agentcore:InvokeGateway; and for Resource, specify the exact ARN of the Gateway. Since arn:aws:iam::<account-id>:root specifies the entire target AWS account as the Principal rather than just the root user, from a least-privilege perspective, it's better to specify a specific IAM Role ARN.

Of course, the calling IAM Role also needs permission for bedrock-agentcore:InvokeGateway.

In the following sections, I'll actually try out the setup and verification steps!

Prerequisites

Account Configuration

This time I'll verify with the following two-account configuration. In an actual environment, the image would look like the following.

CleanShot 2026-06-01 at 23.17.03@2x

Account AWS Account ID Role
Account A (tool provider) 111122223333 Hosts the Gateway
Account B (tool consumer) 444455556666 Calls the Gateway from a different account

Please substitute the account IDs with the values you actually use.

Environment

  • Python 3.13
  • AWS CLI v2 (authenticated to both accounts)
  • boto3 1.43.17
  • mcp 1.27.1
  • mcp-proxy-for-aws 1.5.0
  • Region: us-east-1

Required Packages

Project initialization and package addition
uv init cross-account-gateway-test
cd cross-account-gateway-test
uv add boto3 mcp mcp-proxy-for-aws

To connect from an MCP client to a Gateway with IAM authentication, use mcp-proxy-for-aws, which automatically attaches SigV4 signatures.

https://pypi.org/project/mcp-proxy-for-aws/

Setting Up the Verification Environment

First, create a Gateway with IAM authentication in Account A (tool provider).

If you already have a Gateway on hand, you can skip this section and start reading from "Trying Cross-Account Invocation".

Full setup script (setup_gateway.py)
setup_gateway.py
"""Create a Gateway with IAM authentication and Lambda target in Account A (tool provider)"""

import json
import time
import io
import zipfile

import boto3

# === Configuration ===
REGION = "us-east-1"
PROFILE = "account-a"  # Change to your Account A (tool provider) profile name
GATEWAY_NAME = "cross-account-test"

session = boto3.Session(profile_name=PROFILE, region_name=REGION)
iam = session.client("iam")
lambda_client = session.client("lambda", region_name=REGION)
agentcore = session.client("bedrock-agentcore-control", region_name=REGION)
account_id = session.client("sts").get_caller_identity()["Account"]

def create_gateway_service_role():
    """Create a service role for Gateway"""
    role_name = "AgentCoreGatewayServiceRole"
    trust_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "bedrock-agentcore.amazonaws.com"},
                "Action": "sts:AssumeRole",
            }
        ],
    }

    try:
        role = iam.create_role(
            RoleName=role_name,
            AssumeRolePolicyDocument=json.dumps(trust_policy),
        )
        print(f"Created role: {role['Role']['Arn']}")
    except iam.exceptions.EntityAlreadyExistsException:
        role = iam.get_role(RoleName=role_name)
        print(f"Role already exists: {role['Role']['Arn']}")

    return role["Role"]["Arn"]

def create_test_lambda():
    """Create a test Lambda function"""
    function_name = "cross-account-gateway-test"
    role_name = "CrossAccountTestLambdaRole"

    # Lambda execution role
    trust_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"Service": "lambda.amazonaws.com"},
                "Action": "sts:AssumeRole",
            }
        ],
    }

    try:
        role = iam.create_role(
            RoleName=role_name,
            AssumeRolePolicyDocument=json.dumps(trust_policy),
        )
        iam.attach_role_policy(
            RoleName=role_name,
            PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        )
        print(f"Created Lambda role: {role['Role']['Arn']}")
        print("Waiting for IAM propagation...")
        time.sleep(10)
    except iam.exceptions.EntityAlreadyExistsException:
        role = iam.get_role(RoleName=role_name)

    # Lambda function code
    code = '''
import json

def handler(event, context):
    return {
        "statusCode": 200,
        "body": json.dumps({"message": f"Hello from Gateway! Input: {event}"})
    }
'''
    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
        zf.writestr("lambda_function.py", code)
    zip_buffer.seek(0)

    try:
        response = lambda_client.create_function(
            FunctionName=function_name,
            Runtime="python3.12",
            Handler="lambda_function.handler",
            Role=role["Role"]["Arn"],
            Code={"ZipFile": zip_buffer.read()},
        )
        print(f"Created Lambda: {response['FunctionArn']}")
    except lambda_client.exceptions.ResourceConflictException:
        response = lambda_client.get_function(FunctionName=function_name)
        response = response["Configuration"]
        print(f"Lambda already exists: {response['FunctionArn']}")

    return response["FunctionArn"]

def setup():
    print("=" * 60)
    print("Step 1: Creating Gateway service role")
    print("=" * 60)
    gateway_role_arn = create_gateway_service_role()

    print(f"\n{'=' * 60}")
    print("Step 2: Creating test Lambda function")
    print("=" * 60)
    lambda_arn = create_test_lambda()

    # Grant Lambda Invoke permission to Gateway role
    iam.put_role_policy(
        RoleName="AgentCoreGatewayServiceRole",
        PolicyName="LambdaInvoke",
        PolicyDocument=json.dumps({
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": "lambda:InvokeFunction",
                    "Resource": lambda_arn,
                }
            ],
        }),
    )
    print("Attached Lambda invoke policy to Gateway role")

    print(f"\n{'=' * 60}")
    print("Step 3: Creating Gateway (IAM authentication)")
    print("=" * 60)
    gateway = agentcore.create_gateway(
        name=GATEWAY_NAME,
        description="Cross-account access test gateway",
        protocolType="MCP",
        authorizerType="AWS_IAM",
        roleArn=gateway_role_arn,
    )
    gateway_id = gateway["gatewayId"]
    gateway_url = gateway["gatewayUrl"]
    gateway_arn = gateway["gatewayArn"]
    print(f"Gateway ID:  {gateway_id}")
    print(f"Gateway URL: {gateway_url}")
    print(f"Gateway ARN: {gateway_arn}")

    # Wait until READY
    print("Waiting for Gateway to become READY...")
    while True:
        status = agentcore.get_gateway(gatewayIdentifier=gateway_id)["status"]
        if status == "READY":
            break
        time.sleep(3)
    print("Gateway is READY!")

    print(f"\n{'=' * 60}")
    print("Step 4: Adding Lambda target")
    print("=" * 60)
    target = agentcore.create_gateway_target(
        gatewayIdentifier=gateway_id,
        name="echo-tools",
        targetConfiguration={
            "mcp": {
                "lambda": {
                    "lambdaArn": lambda_arn,
                    "toolSchema": {
                        "inlinePayload": [
                            {
                                "name": "echo_message",
                                "description": "Echo back a message for testing",
                                "inputSchema": {
                                    "type": "object",
                                    "properties": {
                                        "message": {
                                            "type": "string",
                                            "description": "The message to echo",
                                        }
                                    },
                                    "required": ["message"],
                                },
                            }
                        ]
                    },
                }
            }
        },
        credentialProviderConfigurations=[
            {"credentialProviderType": "GATEWAY_IAM_ROLE"}
        ],
    )
    target_id = target["targetId"]
    print(f"Target ID: {target_id}")

    # Wait until READY
    print("Waiting for Target to become READY...")
    while True:
        status = agentcore.get_gateway_target(
            gatewayIdentifier=gateway_id, targetId=target_id
        )["status"]
        if status == "READY":
            break
        time.sleep(3)
    print("Target is READY!")

    # Save configuration
    config = {
        "gateway_id": gateway_id,
        "gateway_url": gateway_url,
        "gateway_arn": gateway_arn,
        "target_id": target_id,
        "lambda_arn": lambda_arn,
        "account_id": account_id,
        "region": REGION,
    }
    with open("gateway_config.json", "w") as f:
        json.dump(config, f, indent=2)

    print(f"\n{'=' * 60}")
    print("Setup complete!")
    print(f"Configuration saved to: gateway_config.json")
    print(f"Gateway URL: {gateway_url}")
    print("=" * 60)

if __name__ == "__main__":
    setup()

Running this creates a Gateway + Lambda target in Account A (tool provider).

Execution command
uv run setup_gateway.py

Once the setup is complete, the connection information is saved in gateway_config.json.

Trying Cross-Account Invocation

To connect to the Gateway, use aws_iam_streamablehttp_client from mcp-proxy-for-aws. Since it automatically attaches SigV4 signatures, you can connect as an MCP client simply by passing the Gateway URL and region.

To make verification easier, it's designed to accept a profile name as an argument for switching.

test_gateway_invoke.py
import asyncio
import os
import sys

import boto3
from mcp import ClientSession
from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client

GATEWAY_URL = "https://<your-gateway-id>.gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp"
AWS_REGION = "us-east-1"

async def invoke_gateway(profile_name: str):
    session = boto3.Session(profile_name=profile_name, region_name=AWS_REGION)
    caller = session.client("sts").get_caller_identity()
    print(f"Profile:  {profile_name}")
    print(f"Account:  {caller['Account']}")
    print(f"Caller:   {caller['Arn']}")
    print("-" * 60)

    os.environ["AWS_PROFILE"] = profile_name
    os.environ["AWS_DEFAULT_REGION"] = AWS_REGION

    try:
        async with aws_iam_streamablehttp_client(
            endpoint=GATEWAY_URL,
            aws_region=AWS_REGION,
            aws_service="bedrock-agentcore",
        ) as (read, write, _):
            async with ClientSession(read, write) as mcp_session:
                await mcp_session.initialize()
                tools = await mcp_session.list_tools()
                print("SUCCESS! tools/list returned:")
                for tool in tools.tools:
                    print(f"  - {tool.name}: {tool.description}")
    except ExceptionGroup as eg:
        for e in eg.exceptions:
            print(f"FAILED: {type(e).__name__}: {e}")
    except Exception as e:
        print(f"FAILED: {type(e).__name__}: {e}")

if __name__ == "__main__":
    profile = sys.argv[1] if len(sys.argv) > 1 else "account-a"
    asyncio.run(invoke_gateway(profile))

First, call from the same Account A (tool provider) to confirm that the Gateway is working correctly.
Specify Account A's profile name (account-a) as the argument.

Calling from the same Account A (tool provider)
uv run test_gateway_invoke.py account-a
Execution result
Profile:  account-a
Account:  111122223333
Caller:   arn:aws:sts::111122223333:assumed-role/AdminRole/session
------------------------------------------------------------
SUCCESS! tools/list returned:
  - echo-tools___echo_message: Echo back a message for testing

It's working without issues. Now let's try calling from Account B (tool consumer).
This time, specify Account B's profile name (account-b) as the argument.

Calling from Account B (tool consumer)
uv run test_gateway_invoke.py account-b
Execution result
Profile:  account-b
Account:  444455556666
Caller:   arn:aws:sts::444455556666:assumed-role/AdminRole/session
------------------------------------------------------------
FAILED: HTTPStatusError: Client error '403 Forbidden' for url 'https://xxxxxxxxxx.gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp'

Oh, it's a 403 Forbidden...

You can't directly access a Gateway with IAM authentication using credentials from a different account. Just like when doing cross-account access with other services, you need to configure the Gateway side to allow access from Account B (tool consumer).

Configuring the Resource-Based Policy

As introduced in the conclusion at the beginning, by configuring a resource-based policy on the Gateway side and IAM permissions on the caller side, cross-account access becomes possible. Let's configure a policy that allows Account B (tool consumer) to call InvokeGateway.

Creating and Attaching the Policy

setup_resource_policy.py
import json
import boto3

GATEWAY_ARN = "arn:aws:bedrock-agentcore:us-east-1:111122223333:gateway/<your-gateway-id>"
REGION = "us-east-1"
CROSS_ACCOUNT_PRINCIPAL = "arn:aws:iam::444455556666:root"

session = boto3.Session(profile_name="account-a", region_name=REGION)  # Account A (tool provider)
client = session.client("bedrock-agentcore-control", region_name=REGION)

policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowCrossAccountInvoke",
            "Effect": "Allow",
            "Principal": {"AWS": CROSS_ACCOUNT_PRINCIPAL},
            "Action": "bedrock-agentcore:InvokeGateway",
            "Resource": GATEWAY_ARN,
        }
    ],
}

client.put_resource_policy(
    resourceArn=GATEWAY_ARN,
    policy=json.dumps(policy),
)
print("Resource policy attached!")
Execution command
uv run setup_resource_policy.py

"*" cannot be used for Resource; you must specify the exact ARN of the Gateway. Also, for Principal, you can narrow it down to a specific IAM Role ARN rather than the entire account.

When allowing only a specific IAM Role
{
  "Principal": {
    "AWS": "arn:aws:iam::444455556666:role/AgentRole"
  }
}

You can also configure this from the Management Console. There's a Resource-based policy section on the Gateway detail screen, where you can enter the policy JSON via the "Add" button.

CleanShot 2026-06-01 at 22.51.28@2x

Trying Cross-Account Invocation Again

With the resource-based policy configured, let's try calling the Gateway from Account B (tool consumer) again.

Execution command
uv run test_gateway_invoke.py account-b
Execution result
Profile:  account-b
Account:  444455556666
Caller:   arn:aws:sts::444455556666:assumed-role/AdminRole/session
------------------------------------------------------------
SUCCESS! tools/list returned:
  - echo-tools___echo_message: Echo back a message for testing

We successfully called the Gateway cross-account!!

By configuring the resource-based policy, we were able to connect to the Gateway with IAM authentication from a different account.

Policy required for the calling IAM Role
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "bedrock-agentcore:InvokeGateway",
      "Resource": "arn:aws:bedrock-agentcore:us-east-1:111122223333:gateway/<your-gateway-id>"
    }
  ]
}

Cleanup

Let's delete the resources when the verification is complete.

Cleanup script (cleanup.py)
cleanup.py
"""Delete the created resources"""

import json
import boto3

REGION = "us-east-1"
PROFILE = "account-a"  # Account A (tool provider)

session = boto3.Session(profile_name=PROFILE, region_name=REGION)
agentcore = session.client("bedrock-agentcore-control", region_name=REGION)
lambda_client = session.client("lambda", region_name=REGION)
iam = session.client("iam")

with open("gateway_config.json") as f:
    config = json.load(f)

gateway_id = config["gateway_id"]
target_id = config["target_id"]
lambda_arn = config["lambda_arn"]

# Delete resource-based policy
try:
    agentcore.delete_resource_policy(resourceArn=config["gateway_arn"])
    print("Deleted resource policy")
except Exception:
    pass

# Delete target
agentcore.delete_gateway_target(
    gatewayIdentifier=gateway_id, targetId=target_id
)
print(f"Deleted target: {target_id}")

# Delete Gateway
agentcore.delete_gateway(gatewayIdentifier=gateway_id)
print(f"Deleted gateway: {gateway_id}")

# Delete Lambda function
lambda_client.delete_function(FunctionName="cross-account-gateway-test")
print("Deleted Lambda function")

# Delete IAM roles
for role_name in ["CrossAccountTestLambdaRole", "AgentCoreGatewayServiceRole"]:
    try:
        # Delete inline policies
        policies = iam.list_role_policies(RoleName=role_name)
        for policy_name in policies["PolicyNames"]:
            iam.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
        # Detach managed policies
        attached = iam.list_attached_role_policies(RoleName=role_name)
        for policy in attached["AttachedPolicies"]:
            iam.detach_role_policy(
                RoleName=role_name, PolicyArn=policy["PolicyArn"]
            )
        iam.delete_role(RoleName=role_name)
        print(f"Deleted role: {role_name}")
    except Exception as e:
        print(f"Error deleting {role_name}: {e}")

print("\nCleanup complete!")
Execution command
uv run cleanup.py

Conclusion

Since Gateway is a service that can bundle multiple APIs, Lambdas, and MCP servers to provide them as a unified MCP endpoint, there are likely many cases where you'd want to centralize it in a specific account and call it from other accounts.
Even in such cases, with IAM authentication, you can access it cross-account without issues by configuring a resource-based policy!

I hope this article was helpful in some way. Thank you for reading to the end!

Share this article