Internal Only Policy
The Internal Only policy admits a request only when this gateway made it with
context.invokeRoute, and rejects every request that arrived over the network.
Use it on routes that only other routes, handlers, and policies on the same
gateway call: REST routes that an MCP server exposes as tools, or the classifier
application behind the Smart Router or Prompt Injection Protection policies.
Those calls never leave the gateway, so an API key on them protects nothing. A
rejected request gets 403 Forbidden by default; set status to 404 to
answer as if the route did not exist. The policy makes no network calls and
never reads the request body.
Configuration
The configuration shows how to configure the policy in the 'policies.json' document.
Code
Policy Configuration
name<string>- The name of your policy instance. This is used as a reference in your routes.policyType<string>- The identifier of the policy. This is used by the Zuplo UI. Value should beinternal-only-inbound.handler.export<string>- The name of the exported type. Value should beInternalOnlyInboundPolicy.handler.module<string>- The module containing the policy. Value should be$import(@zuplo/runtime).handler.options<object>- The options for this policy. See Policy Options below.
Policy Options
The options for this policy are specified below. All properties are optional unless specifically marked as required.
status<integer>- The HTTP status code returned to a request that did not come from inside the gateway. Any client error status from 400 to 499 is allowed. Use404to answer as if the route did not exist, or401to answer as an authentication policy does. Defaults to403.statusText<string>- The status text returned withstatus, such asNot Found. When it is omitted, the response uses the standard text for the status. Clients don't always receive it: HTTP/2 and HTTP/3 have no status text, and Managed Dedicated and self-hosted gateways always send the standard text.detail<string>- Thedetailof the problem response returned to a rejected request. Every caller outside the gateway receives it, so don't describe how the route is called. When it is omitted, the response has nodetail, and a404matches the response for a route that does not exist.
Using the Policy
Add this policy first in the inbound policies of each route that only your
gateway calls. With no options it rejects requests from outside the gateway with
403 Forbidden; set status, statusText, and detail to change the
response.
How it works
context.invokeRoute runs a route on this gateway without leaving the process,
and the runtime marks the request it creates by setting context.parentContext
to the calling request's context. Nothing else sets that property. A request
that arrives over the network never has one, whatever headers, URL, or body it
carries. This policy passes a request when context.parentContext is present
and rejects it otherwise.
| Request origin | Result |
|---|---|
| Any HTTP client, with or without credentials | Rejected |
context.invokeRoute from a policy, handler, or custom module on this gateway | Passes |
| The MCP Server handler calling the route as a tool | Passes |
| An AI Gateway application calling another application on the same gateway | Passes |
The policy does not authenticate the caller and does not set request.user.
Choose the response
By default, a rejected request gets a 403 Forbidden problem response with no
detail, so the caller learns nothing about how your gateway works. The policy
writes the reason to the request log at debug level, where you can see it and
the caller can't.
Set status to answer with any client error status from 400 to 499,
statusText to replace the status text, and detail to add a message:
options | Response to a request from outside the gateway |
|---|---|
{} | 403 Forbidden with no detail |
{ "status": 404 } | 404 Not Found with no detail, as for a route that does not exist |
{ "status": 401 } | 401 Unauthorized with no detail |
{ "status": 401, "statusText": "Access Denied" } | 401 Access Denied with no detail |
{ "detail": "Access denied." } | 403 Forbidden with your detail |
Every caller outside the gateway receives your detail, so don't use it to
explain how the route is called or what protects it.
statusText replaces only the status text; the problem's title stays the
standard one, such as Unauthorized. Clients don't always receive it: HTTP/2
and HTTP/3 have no status text, and Managed Dedicated and self-hosted gateways
always send the standard text.
Hide REST routes behind an MCP server
The MCP Server handler calls each tool's route with context.invokeRoute, so
the tools keep working while direct calls to those REST routes get the same
404 as a route that does not exist. Declare the policy with "status": 404:
Code
Put it first in the inbound policies of each REST route that the MCP server
exposes, so nothing else runs for a rejected request. Mark the route
x-internal to keep it out of your
developer portal as well:
Code
The policy's 404 is the gateway's standard not-found problem. Two things can
still tell a hidden route from one that doesn't exist:
- Preflight
OPTIONSrequests skip inbound policies, so a route with a CORS policy still answers them. SetcorsPolicytononeon hidden routes, as in the example. - If you set
runtime.notFoundHandlerin your runtime extensions, paths that don't exist answer with your handler's response instead.
The MCP server route lists the operation as a tool and authenticates its own callers:
Code
Protect an AI Gateway application
New AI Gateway projects declare this policy as internal-only-inbound. Select
it in an application's inboundPolicyChain in place of
ai-gateway-auth-inbound, and put it first so nothing else runs for a rejected
request:
Code
A chain entry's options replace the declared options for that application
only:
Code
AWS Bedrock Runtime requests get the AWS error envelope instead of a problem
response, so the AWS SDKs can parse the error: AccessDeniedException for a
401 or 403, and ResourceNotFoundException for a 404. Its message is
your detail, or the status title, such as Forbidden, when you set none. That
holds in an application's chain and on the AI Gateway route itself, ahead of the
configuration loader.
Metering records these requests against the application with no consumer, and a
budget rule keyed on request.user has no value to accumulate for them. Attach
per-consumer budgets to the calling application instead.
The Smart Router and Prompt Injection Protection policies call their classifier applications this way.
Call a protected route
Call the route from a policy or handler on the same gateway. No credentials are needed:
Code
A direct call from outside the gateway is rejected:
Code
With the default options the response is 403 Forbidden with no detail. The
request log records why the policy rejected it.
What it does not check
This policy asserts that a request originated inside this gateway deployment,
not that the caller is trusted. Any route on the same gateway that forwards
requests through context.invokeRoute, including an MCP server that exposes the
route as a tool, reaches it. Protect those routes with their own authentication,
exactly as you would an internal URL.
Deprecated names
This policy began in the AI Gateway as Ensure Gateway Internal Invocation Only.
Its earlier names are deprecated: the AIGatewayInternalOnlyInboundPolicy
export, the ai-gateway-internal-only policy type, and the
ai-gateway-internal-only-inbound chain-entry name. Configurations that use
them keep working unchanged, but new configurations should use
InternalOnlyInboundPolicy and internal-only-inbound.
Read more about how policies work