AWS Builder Center
Enforce layered, end-to-end access control for AI agents with Amazon Bedrock AgentCore, Amazon Cognito, and Cedar

Enforce layered, end-to-end access control for AI agents with Amazon Bedrock AgentCore, Amazon Cognito, and Cedar

Learn how to implement three independent, deterministic authorization controls for AI agents—identity propagation with Amazon Cognito, per-agent tool restrictions with a Gateway interceptor, and per-user tool policies with Cedar—all enforced at the Amazon Bedrock AgentCore Gateway, outside the model's reasoning.

As organizations move generative AI agents from prototype to production, a hard question surfaces: who is allowed to do what? An agent that can read device telemetry is useful; an agent that can silently change a device's Wi‑Fi security settings on behalf of the wrong user is a liability. Because an agent's behavior is driven by a large language model (LLM), you can't rely on prompt instructions alone to enforce access—a determined prompt can talk its way around guidance. Authorization has to be deterministic and enforced outside the model's reasoning.
In this post, you learn how to implement layered, end‑to‑end access control for an AI agent using Amazon Bedrock AgentCore , Amazon Cognito , and Cedar  policies. You build three independent, complementary controls that together answer the "user → agent → tool" authorization question:
  1. Identity (OAuth 2.0/JWT) – Authenticate the user with Amazon Cognito and propagate that identity, end to end, to the gateway that fronts the tools.
  2. Agent‑level control (Gateway interceptor) – Restrict which tools an agent can call, independent of the user.
  3. User‑level control (AgentCore policy engine and Cedar) – Restrict which tools a specific user can call, independent of the agent.
You use a device management assistant as the running example: an agent that can list devices, read settings and Wi‑Fi networks, list users, query activity, and update Wi‑Fi SSID and security settings through tools exposed by an AgentCore Gateway.

Use case: a device management assistant

Consider a conversational assistant that helps teams operate a fleet of smart‑home and IoT devices. Through natural language, it can list devices, read device and Wi‑Fi settings, list users, query activity, and update Wi‑Fi SSID and security settings—each backed by a tool on the AgentCore Gateway.
Not everyone should have the same power. Reading telemetry is low‑risk; changing a network's security type is high‑risk. This post uses two personas to make the controls concrete:
  • A support analyst investigates issues but must not change anything. They're routed to a read‑only agent (Control 2: agent → tools).
  • A field technician fixes connectivity and can update a Wi‑Fi SSID, but must not weaken a network's security settings. They use the full agent, with a user‑level rule that forbids the security‑update tool (Control 3: user → tools).
Both personas sign in through Amazon Cognito, and their identity flows all the way to the gateway (Control 1), where the rules are enforced. The rest of this post shows how to build each control.

The challenge

A production agent typically sits behind a chain of hops: a user in a browser calls a frontend, the frontend invokes the agent in AgentCore Runtime, the agent calls tools through an AgentCore Gateway, and the gateway invokes AWS Lambda functions that read and write data. Each hop is a place where authorization can be lost or bypassed:
  • If the user's identity isn't carried through to the gateway, you can only enforce coarse, machine‑to‑machine rules—you can't say "user A can't do X."
  • If you enforce tool access only in the agent's prompt, the LLM can be coaxed into calling a restricted tool anyway.
  • If different agents share one gateway, you need a way to give each agent a different slice of the tool surface.
The solution is to enforce authorization at the gateway—the deterministic choke point in front of the tools—and to do it at two granularities (per agent and per user), anchored by a verifiable user identity.

Solution overview

The following diagram shows the request flow and where each control is applied.
The three controls are independent and layered, providing defense in depth:
LayerMechanismQuestion it answersWhere enforced
IdentityAmazon Cognito OAuth 2.0 and JWTWho is calling?Runtime and Gateway inbound authorizers
Agent → toolsGateway request interceptor (Lambda) and allowlistWhich tools can this agent use?Gateway, before the tool runs
User → toolsAgentCore policy engine and CedarWhich tools can this user use?Gateway, before the tool runs
Because all three controls are enforced at the gateway rather than in the prompt, they hold even under adversarial prompting.

Prerequisites

To follow along, you need the following:
  • An AWS account with access to Amazon Bedrock AgentCore in a supported AWS Region 
  • An Amazon Cognito user pool and an app client configured for the authorization code grant, with an OpenID Connect (OIDC) discovery URL
  • An AgentCore Runtime hosting your agent (this example uses the Strands Agents  SDK) and an AgentCore Gateway that exposes your tools over the Model Context Protocol (MCP)
  • The latest AgentCore SDK (bedrock-agentcore) and a recent version of the AWS SDK for Python (boto3)
  • Familiarity with OAuth 2.0 (JWT access tokens and claims) and basic AWS Identity and Access Management (IAM)

Control 1: Authenticate the user and propagate identity end to end

Use a single Amazon Cognito user pool and app client across the frontend, the runtime, and the gateway. A single trust anchor means one OIDC discovery URL and one set of allowed clients—and, crucially, the same user access token validates at every hop, so the user's identity reaches the gateway intact.
Configure the AgentCore Runtime and Gateway with a JWT (OIDC) inbound authorizer. For Amazon Cognito access tokens, validate the client_id claim with allowedClients (don't set allowedAudience, because Cognito access tokens don't carry an aud claim):
1
2
3
4
5
6
auth_config = {
"customJWTAuthorizer": {
"allowedClients": [COGNITO_APP_CLIENT_ID],
"discoveryUrl": f"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/openid-configuration",
}
}
An AgentCore Runtime uses either IAM SigV4 or JWT bearer inbound authentication—not both. For user‑facing OAuth applications, choose JWT bearer so the user's identity flows through.
Next, allow the runtime to read the inbound Authorization header, and have the agent forward the same user token when it calls the gateway. This is what makes per‑user authorization possible downstream:
1
2
3
4
5
6
7
# At runtime creation: let the container read the user's JWT
requestHeaderConfiguration = {"requestHeaderAllowlist": ["Authorization"]}

# In the agent, reuse the inbound user token for the outbound MCP/Gateway call
auth_header = context.request_headers.get("Authorization", "")
user_jwt = auth_header[7:] if auth_header.lower().startswith("bearer ") else auth_header
headers = {"Authorization": f"Bearer {user_jwt}"}
The frontend invokes the runtime over HTTPS with the user's access token:
1
2
3
4
5
6
7
8
url = (
f"https://bedrock-agentcore.{REGION}.amazonaws.com"
f"/runtimes/{quote(agent_arn, safe='')}/invocations?qualifier=DEFAULT"
)
headers = {
"Authorization": f"Bearer {user_access_token}",
"Accept": "text/event-stream",
}
Note: The runtime's serverProtocol must be HTTP for an agent that is invoked over HTTP and consumes MCP tools from the gateway. Setting it to MCP tells the platform that the container itself is an MCP server, and the agent's HTTP entry point never receives requests.

Route users to agents (optional)

You can run more than one agent against the same gateway and choose which agent a user gets. Store a username → agent ARN map in AWS Systems Manager Parameter Store , a capability of AWS Systems Manager, and select the agent in the frontend based on the JWT's username claim:
1
2
3
4
# SSM parameter: /device-mgmt/frontend/user-agent-mapping
# {"analyst": "<read-only-agent-arn>", "technician": "<full-agent-arn>"}
claims = decode_jwt_claims(access_token) # runtime already validated the token
agent_arn = user_agent_map.get(claims.get("username"), DEFAULT_AGENT_ARN)

Control 2: Restrict tools per agent with a Gateway interceptor

An AgentCore Gateway interceptor is a Lambda function that the gateway invokes before it routes a tool call. Each agent identifies itself with a custom x-agent-id header; the interceptor looks up the agent in an allowlist (stored in Parameter Store) and denies any tool that isn't on the list:
1
2
3
4
5
6
7
8
9
10
11
12
def lambda_handler(event, context):
req = event["mcp"]["gatewayRequest"]["body"]
headers = event["mcp"]["gatewayRequest"]["headers"]
agent_id = headers.get("x-agent-id", "unknown")

if req.get("method") == "tools/call":
allowed = get_allowlist().get(agent_id) # {agent_id: [tool, ...]}
tool = req["params"]["name"]
if allowed is not None and tool not in allowed:
return deny(req, f"Agent '{agent_id}' is not authorized to call {tool}")

return passthrough(req)
Attach the interceptor to the gateway and allow the custom header to reach it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
client.update_gateway(
gatewayIdentifier=GATEWAY_ID,
# ... existing name/role/authorizer ...
interceptorConfigurations=[{
"interceptor": {"lambda": {"arn": INTERCEPTOR_LAMBDA_ARN}},
"interceptionPoints": ["REQUEST"],
"inputConfiguration": {"passRequestHeaders": True},
}],
)

client.update_gateway_target(
gatewayIdentifier=GATEWAY_ID,
targetId=TARGET_ID,
name=TARGET_NAME,
targetConfiguration=target_config,
metadataConfiguration={"allowedRequestHeaders": ["x-agent-id"]},
)
With a read‑only agent registered as device-readonly-agent (the support analyst's agent) and an allowlist that grants it only the read tools, any attempt to call update_wifi_ssid or update_wifi_security is denied at the gateway—regardless of what the model decides to do.

Control 3: Restrict tools per user with the policy engine and Cedar

For user‑level rules, attach an AgentCore policy engine to the gateway and author Cedar policies. Cedar is deny by default, so you begin with a permit for all authenticated users, then add targeted forbid statements. Because the user's JWT reaches the gateway (Control 1), Cedar can evaluate the username claim as a principal tag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Permit all authenticated users (Cedar is deny by default)
permit(
principal is AgentCore::OAuthUser,
action,
resource == AgentCore::Gateway::"<gateway-arn>"
);

// Forbid the field technician from changing Wi-Fi security
forbid(
principal is AgentCore::OAuthUser,
action == AgentCore::Action::"DeviceLambdaTarget___update_wifi_security",
resource == AgentCore::Gateway::"<gateway-arn>"
) when {
principal.hasTag("username") && principal.getTag("username") == "technician"
};
Attach the engine in ENFORCE mode, preserving the interceptor configuration in the same update call:
1
2
3
4
5
6
client.update_gateway(
gatewayIdentifier=GATEWAY_ID,
# ... name/role/authorizer ...
interceptorConfigurations=existing_interceptor_configs, # keep Control 2
policyEngineConfiguration={"arn": POLICY_ENGINE_ARN, "mode": "ENFORCE"},
)
When an enforced user is forbidden a tool, the policy engine removes it from that user's available toolset—the agent can't even see, let alone call, the restricted tool.

Test the controls

You can validate each layer independently:
  • Identity – A signed‑in user's request reaches the gateway with the real username claim (confirmed in the interceptor logs as user=<username>), not an anonymous machine token.
  • Agent → tools – The support analyst's read‑only agent successfully calls list_devices, but a forced update_wifi_ssid returns Access Denied: Agent 'device-readonly-agent' is not authorized to call ....
  • User → tools – The field technician can still call update_wifi_ssid and the read tools, but update_wifi_security is removed from their toolset and can't be invoked.

Security best practices

  • Validate tokens server side at every hop – Check signature, issuer, expiry, and the appropriate client and scope claims.
  • Prefer deterministic, gateway‑side enforcement over prompt instructions for anything security‑relevant.
  • Keep secrets out of code and .env files – Store configuration in AWS Systems Manager Parameter Store  (use SecureString for secrets, or AWS Secrets Manager  when you need rotation) and grant least‑privilege ssm:GetParameter* and kms:Decrypt permissions.
  • Apply least privilege to IAM roles – Scope the runtime execution role to only the services it calls, and make sure it can write its own Amazon CloudWatch  logs (the runtime log group is /aws/bedrock-agentcore/runtimes/<runtime-id>-DEFAULT).
  • Fail closed – If a user token is absent or invalid, deny the request rather than falling back to a privileged path.
  • Pin dependencies and container images – Use exact versions and immutable image tags (never :latest) for reproducible, auditable deployments.
  • Log and alert on authorization decisions, and never log tokens or secrets.

Clean up

To avoid ongoing charges, delete the resources you created:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Detach Cedar + interceptor, then remove policies/engine and the read-only runtime
aws bedrock-agentcore-control delete-policy \
--policy-engine-id <engine-id> \
--policy-id <policy-id>

aws bedrock-agentcore-control delete-policy-engine \
--policy-engine-id <engine-id>

aws bedrock-agentcore-control delete-agent-runtime \
--agent-runtime-id <readonly-runtime-id>

# Remove SSM parameters, the interceptor Lambda, the gateway, target, and
# Cognito resources you created

Conclusion

By combining Amazon Cognito OAuth identity, an AgentCore Gateway interceptor, and an AgentCore policy engine with Cedar, you enforce authorization for AI agents at three independent granularities—who is calling, which agent is calling, and which tools that user can use—deterministically, at the gateway, outside the model's reasoning. This layered model lets you safely expand what your agents can do while keeping a tight, auditable boundary on what each user and each agent is actually allowed to do.
To get started, explore the Amazon Bedrock AgentCore documentation  and the amazon-bedrock-agentcore-samples  repository on GitHub.

About the author

Godwin Sahayaraj Vincent is a Senior Solutions Architect at AWS, where he helps telco customers design and operate secure generative AI applications. He focuses on agentic AI, IoT, and cloud‑native architectures.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article