A public endpoint is unauthenticated by design. Anyone who knows the URL can query your indexed content.
Put a custom domain in front of the public endpoint and protect it with Cloudflare Access. Users then authenticate with your identity provider before any request reaches AI Search. This turns a public knowledge base into an internal one without writing an authentication layer.
A custom domain is a hostname in a zone that you own. When the CNAME record for that hostname is Proxied, requests pass through your own zone before they reach AI Search:
flowchart LR A[Client] --> B["Your zone<br/>Access, WAF, Bots"] B --> C["AI Search<br/>public endpoint"] C --> D[Your indexed content]
Access runs in your zone, so it evaluates every request first. Requests that fail a policy never reach AI Search. AI Search needs no configuration to support this and applies its own rate limits afterwards.
This routing is called orange-to-orange. It requires the CNAME record to be proxied. A DNS only record bypasses your zone entirely, and Access never runs.
- A custom domain on the instance or namespace you want to protect, with a Proxied
CNAMErecord. - Cloudflare Access enabled on your account.
- An identity provider connected to Cloudflare Access, or Cloudflare's one-time PIN.
Access only protects your custom domain. The default <PUBLIC_ENDPOINT_ID>.search.ai.cloudflare.com hostname does not pass through your zone, so it keeps answering unauthenticated requests and defeats the policy you are about to write.
Set default_domain_enabled to false before you create the Access application.
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai-search/namespaces/default/instances/<INSTANCE_ID>" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"public_endpoint_params": {
"enabled": true,
"custom_domains": ["access.search.example.com"],
"default_domain_enabled": false
}
}'The default hostname now returns a 404 with error 60018.
- In the Cloudflare dashboard ↗, go to Zero Trust > Access controls > Applications.
- Select Add an application > Self-hosted.
- Name the application, for example
AI Search. - Add the public hostname of your custom domain, for example
access.search.example.com. - Save the application.
For the full set of options, refer to Publish a self-hosted application.
An application with no policy denies every request. Add at least one Allow policy that describes who may query your content.
| Action | Rule type | Selector | Value |
|---|---|---|---|
| Allow | Include | Emails ending in | @example.com |
Use the email, country, IP range, or identity provider group selectors to match your organization. Refer to Common policies for more examples.
Browsers follow the Access login redirect and receive a CF_Authorization cookie. MCP clients, backend services, and scripts cannot complete an interactive login, so they need a service token.
-
Go to Zero Trust > Access controls > Service credentials > Service Tokens.
-
Select Create Service Token, name it, and choose a duration.
-
Copy the Client ID and Client Secret. The secret is shown only once.
-
Return to your Access application and add a second policy:
Action Rule type Selector Value Service Auth Include Service Token ai-search-client
Send both credentials as headers on every request.
curl https://access.search.example.com/search \
--header "Content-Type: application/json" \
--header "CF-Access-Client-Id: <CLIENT_ID>" \
--header "CF-Access-Client-Secret: <CLIENT_SECRET>" \
--data '{
"messages": [
{
"content": "How do I configure AI Search?",
"role": "user"
}
]
}'{
"mcpServers": {
"ai-search": {
"url": "https://access.search.example.com/mcp",
"headers": {
"CF-Access-Client-Id": "<CLIENT_ID>",
"CF-Access-Client-Secret": "<CLIENT_SECRET>"
}
}
}
}Header support varies by MCP client. If your client cannot send custom headers, refer to Secure MCP servers for identity-based alternatives, or use cloudflared access curl for command-line requests.
A request without credentials returns the Access login page instead of search results:
curl --include https://access.search.example.com/search \
--header "Content-Type: application/json" \
--data '{"messages":[{"content":"test","role":"user"}]}'Confirm that the default hostname is closed:
curl https://<PUBLIC_ENDPOINT_ID>.search.ai.cloudflare.com/search \
--header "Content-Type: application/json" \
--data '{"messages":[{"content":"test","role":"user"}]}'The response is a 404 with error code 60018.
- UI snippets on a public website stop working. UI snippets call the public endpoint from the visitor's browser. Behind Access, only visitors who pass your policies can use them. Use Access for internal sites, and leave the endpoint open for public marketing sites.
- Cross-origin browser requests need an Access CORS configuration. If a page on a different origin calls the protected hostname, configure CORS settings on the Access application in addition to the allowed origins on the public endpoint.
- Rate limits still apply. AI Search enforces its own rate limit after Access, and it is shared across all authenticated callers.
- Allowed origins are not authentication. The
authorized_hostssetting sets CORS response headers, which only browsers honor. It does not stop a direct request fromcurlor a script.
Once the CNAME record is proxied, other Cloudflare products also apply to the hostname:
| Product | Use it to |
|---|---|
| WAF custom rules | Allow specific countries, ASNs, IP ranges, or headers |
| Rate limiting rules | Apply per-client limits beyond the public endpoint limit |
| Bot Management | Score and challenge automated traffic |
| Turnstile | Verify humans before a browser client calls the endpoint |