AWS Builder Center
Per-agent tool access for Kong-protected APIs with Amazon Bedrock AgentCore Gateway

Per-agent tool access for Kong-protected APIs with Amazon Bedrock AgentCore Gateway

Two agents share one gateway and one Kong-protected API, and only one of them can write. An AWS Lambda interceptor shows and allows each agent only the tools its OAuth scopes grant, the API is reached privately, and a denied call never leaves the gateway.

If your REST APIs already sit behind Kong Gateway,  Amazon Bedrock AgentCore Gateway  can offer them to your agents as Model Context Protocol (MCP) tools, with no change to the APIs or to Kong. Giving each agent only the tools its job needs takes one more piece: a small interceptor that maps each agent's OAuth scopes to the tools it can see and call.
In this post, you deploy a working example with two agents, a reader and an operator, that share one gateway and one Kong-protected API. You see how the interceptor shapes each agent's tool list and enforces it on every call, how the gateway reaches Kong over a private network path, and how this approach compares with the other ways to set tool policy. The deploy takes about 10 minutes.

The challenge

When you put AgentCore Gateway in front of an existing REST API, every operation in its OpenAPI spec becomes an MCP tool. By default, every agent that can authenticate to the gateway sees every one of those tools. A read-only reporting agent gets offered  setSiteStatus  right next to  getSite . Least privilege says it shouldn't even know that tool exists.
Kong already answers a different question. In Kong Gateway (OSS), the  key-auth  plugin decides whether a request can reach the API at all, but it doesn't decide which MCP tools each agent is offered. This post keeps Kong doing what it already does, and puts per-agent tool policy in the AgentCore Gateway.
The full sample is on GitHub at  aws-samples/sample-agentcore-gateway-kong-tool-access  under the MIT-0 license. It's one  AWS Cloud Development Kit (AWS CDK)  stack and one  cdk deploy .

Three layers, three questions


  1. Who?  Amazon Cognito  issues client-credentials tokens that expire after 15 minutes. The  reader  client gets  sites.read ; the  operator  client gets  sites.read sites.write . The gateway's JWT authorizer accepts only these two clients.
  1. Which tools? An  AWS Lambda  interceptor runs on both REQUEST and RESPONSE. It filters  tools/list  to the tools the caller's scopes allow, and refuses a disallowed  tools/call  before the target is called.
  1. Which APIs? The gateway adds Kong's  apikey  header from an  Amazon Bedrock AgentCore Identity  API key provider, and Kong accepts only requests that carry it. The gateway reaches Kong over a  private path :  Amazon VPC Lattice , then an internal  Application Load Balancer  (ALB) that accepts traffic only from the VPC Lattice resource gateway. The ALB forwards to Kong over HTTPS as well, so the key is never sent in plaintext.
The interceptor pattern itself comes from the AWS post  Apply fine-grained access control with Bedrock AgentCore Gateway interceptors . What's new here is applying it to a Kong-protected API, with read and write tools on one target.

The interceptor, in a few lines

The policy is a plain map from scope to tool names, set by the AWS CDK as an environment variable:
JSON
{ "kta-api/sites.read": ["kta-sites___getSite"],
"kta-api/sites.write": ["kta-sites___setSiteStatus"] }
At REQUEST time, a call to a tool the caller wasn't granted gets a JSON-RPC error, and the target is never invoked. This excerpt is simplified: the full handler also lets any caller with a valid token use the gateway's built-in search tool, because the search results are filtered on the way back.
Python
def _on_request(body, claims):
if body.get("method") != "tools/call":
return _pass_request(body) # initialize, ping, tools/list ...
name = (body.get("params") or {}).get("name")
allowed = isinstance(name, str) and name in _allowed(claims)
logger.info(json.dumps({"point": "REQUEST", "tool": name,
"caller": _caller(claims), "decision": "allow" if allowed else "deny"}))
return _pass_request(body) if allowed else _deny(body, name) # -32000 Forbidden
At RESPONSE time, the same function removes tools the caller wasn't granted from  tools/list  (and from semantic-search results), so the agent never learns they exist. A missing token, an unknown scope, an unknown tool, a batched request, a malformed  tools/list  response or any exception all hide or deny. It fails closed, and the repo's unit tests cover each case.
One design choice to be explicit about: the interceptor decodes the JWT to read  scope , but it doesn't verify the signature. That's safe only because the gateway's JWT authorizer has already validated the token before the interceptor runs, and the stack grants invoke permission only to the gateway's role. Another principal in the account with broad Lambda permissions could still call the function directly, but it would get back only a filtered list or an error, never a path to the API, because only the gateway holds Kong's key. If you reuse the function anywhere else, add JWKS verification.

Wiring it up in the AWS CDK

The AWS CDK library  aws-cdk-lib  2.273.0 has L2 constructs for the AgentCore Gateway, its interceptors, OpenAPI targets and API key providers, so the whole demo is one stack:
ts
const gateway = new Gateway(this, 'Gateway', {
gatewayName: 'kta-gw',
authorizerConfiguration: GatewayAuthorizer.usingCognito({ userPool: pool, allowedClients: [reader, operator] }),
interceptorConfigurations: [
LambdaInterceptor.forRequest(interceptor, { passRequestHeaders: true }), // needs the caller's token
LambdaInterceptor.forResponse(interceptor, { passRequestHeaders: true }),
],
});
const provider = new ApiKeyCredentialProvider(this, 'ApiKeyProvider', {
apiKeyCredentialProviderName: 'kta-kong-apikey',
apiKey: kongKey.secretValueFromJson('api_key'), // dynamic reference, no plaintext
});
const target = GatewayTarget.forOpenApi(this, 'SitesTarget', { gateway, apiSchema, credentialProviderConfigurations: [/* apikey header */] });
(target.node.defaultChild as CfnGatewayTarget).privateEndpoint = { managedVpcResource: { vpcIdentifier, subnetIds, securityGroupIds } };
The same  AWS Secrets Manager  secret is injected into the Kong container by  Amazon Elastic Container Service (Amazon ECS)  on  AWS Fargate , and fed to the API key provider through an  AWS CloudFormation  dynamic reference, so the key never appears in the repo or the template. One customer managed  AWS Key Management Service (AWS KMS)  key, with automatic rotation, encrypts that secret, the three log groups and the interceptor's environment variables. Its key policy names every action, with no wildcards, and lets  Amazon CloudWatch Logs  use the key only for this stack's log groups.

Prerequisites

  • An AWS account for development or testing, bootstrapped for the AWS CDK ( npx cdk bootstrap ), with a  default VPC .
  • A public  Amazon Route 53  hosted zone. AgentCore Gateway requires an  https  target URL with a publicly trusted certificate, even for a private target, so the stack issues an  AWS Certificate Manager (ACM)  certificate and an alias record for the internal ALB.
  • Node.js 22 or later, Python 3.12 or later (on Amazon Linux 2023,  python3  is 3.9, so use  python3.12 ), and  AWS Command Line Interface (AWS CLI)  v2. You don't need Docker: Amazon ECS pulls the Kong and Python images, pinned by digest.

Run it

Bash
npm ci && npm test # tsc + jest + cdk-nag
python3.12 -m venv .venv && .venv/bin/pip install -r demo/requirements.txt
npx cdk deploy -c domainName=kta-api.example.com -c hostedZoneId=Z0123456789ABC -c hostedZoneName=example.com
PYTHON=.venv/bin/python ./demo/demo.sh
The deploy takes about 10 minutes, about half of it while AgentCore creates the VPC Lattice resource gateway.
The demo prints each caller's scopes (decoded locally, never the token), the live policy, and the interceptor's own logged decision at every step:
Plain text
1. reader -> tools/list
Got kta-sites___getSite
Log interceptor: tools/list filtered 2 -> 1 PASS
2. operator -> tools/list
Got kta-sites___getSite kta-sites___setSiteStatus PASS
3. reader -> tools/call kta-sites___setSiteStatus (a tool it was never shown)
Got {"error": {"code": -32000, "message": "Forbidden: tool 'kta-sites___setSiteStatus' is not permitted for this caller"}}
Log interceptor: decision=deny tool=kta-sites___setSiteStatus PASS
4. operator -> tools/call kta-sites___setSiteStatus
Path Gateway -> VPC Lattice -> internal ALB -> Kong key-auth -> API
Got {"result": {"siteId": "SITE-001", "name": "Example Site A", "status": "paused"}} PASS
4/4 PASS
Kong's access log shows exactly one  PUT /sites/SITE-001/status , the one from step 4. Step 3 never left the gateway.

Good to know

  • Configure outbound auth on the target, not in the spec. OpenAPI targets take credentials from the target configuration, so the repo's spec documents Kong's  apikey  header and the stack removes the spec-level  securitySchemes  before creating the target.
  • Send Kong the raw API key. The L2 defaults the header prefix to  Bearer  , Kong expects the raw key, and CloudFormation requires a non-empty prefix value, so the stack removes the prefix property.
  • Turn on application logs for target errors. The agent sees a target error only as "An internal error occurred." The stack turns on the gateway's  APPLICATION_LOGS , which record the full cause.
  • Keep the ALB, Fargate task and resource gateway in the same subnets. The ALB sends traffic only to targets in the Availability Zones it covers.
  • Denials arrive as a JSON-RPC error, not as an  isError  tool result, so MCP clients raise a normal tool error.
  • A self-signed certificate on Kong is enough. The ALB encrypts traffic to Kong and accepts target certificates from any issuer. AWS documents that traffic between an ALB and its targets inside a VPC is authenticated at the packet level, which protects it from spoofing even with a self-signed certificate.
  • The demo's Kong task has a public IP for image pulls. It lets the task pull images from Docker Hub in a default VPC without a NAT gateway. Its security group accepts traffic only from the ALB, and outbound only on TCP 443, so Kong and the API stay unreachable from the internet. In production, use private subnets and pull images from  Amazon Elastic Container Registry (Amazon ECR)  through VPC endpoints.

Which tool-policy option to choose

These are the options as of November 2026:
  • Gateway interceptor (this post): the most flexible option. A few dozen lines of code you own, with filtering at discovery time and enforcement at call time.
  •  Policy in Amazon Bedrock AgentCore  (Cedar): declarative rules evaluated on every tool call, with no code to maintain. A good fit when you want auditable, parameter-aware rules.
  • Kong AI Gateway: if Kong is already your policy point for MCP, the  AI MCP Proxy  plugin supports  per-tool ACLs  by consumer group, with a default rule and per-tool overrides. It's part of Kong's AI Gateway Enterprise offering and needs Kong Gateway 3.12 or later, or Konnect.
Whichever you pick, choose one place to own tool policy, and key it on the same identity-provider scopes.

Clean up

Bash
npx cdk destroy --force -c domainName=kta-api.example.com -c hostedZoneId=Z0123456789ABC -c hostedZoneName=example.com
The Fargate task, the internal ALB and the VPC Lattice resource gateway are billed while they run, so destroy the stack when you're done.  cdk destroy  schedules the AWS KMS key for deletion with a 7-day waiting period, so the key stays visible, disabled and pending deletion, for a week. ACM leaves its  _<hash>.<domainName>  validation CNAME in your hosted zone, and Amazon ECS leaves the  Amazon CloudWatch Container Insights  log group  /aws/ecs/containerinsights/kta-cluster/performance , so delete both by hand.

Next steps

To grant a new permission, add a scope to the Amazon Cognito resource server and a line to the policy map; the interceptor code doesn't change. If you'd rather declare rules than maintain code, put the same scope-to-tool mapping in Policy in Amazon Bedrock AgentCore instead. Questions and pull requests are welcome on the  repo .
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